@echomem/mcp 1.4.22 → 1.4.23

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/hud/web.js CHANGED
@@ -1723,7 +1723,7 @@ export const HUD_HTML = String.raw `<!doctype html>
1723
1723
  if (previewParams.get("capture") === "1") hud.classList.add("capture-preview");
1724
1724
  if (previewColor === "green" || previewColor === "amber" || previewColor === "red") {
1725
1725
  const previewProcessingUsed = previewColor === "red" ? 200000 : previewColor === "amber" ? 163000 : 84000;
1726
- const previewSearchUsed = previewColor === "red" ? 100 : previewColor === "amber" ? 82 : 32;
1726
+ const previewSearchUsed = previewColor === "red" ? 500 : previewColor === "amber" ? 410 : 160;
1727
1727
  billingStatus = {
1728
1728
  ok: true,
1729
1729
  state: "ok",
@@ -1731,8 +1731,8 @@ export const HUD_HTML = String.raw `<!doctype html>
1731
1731
  paid: true,
1732
1732
  pricingUrl: "https://yeahecho.com/pricing?source=hud_preview",
1733
1733
  memoryProcessingQuota: { used: previewProcessingUsed, limit: 200000, remaining: 200000 - previewProcessingUsed, resetAt: new Date(Date.now() + 31 * 60 * 60 * 1000).toISOString() },
1734
- memorySearchQuota: { used: previewSearchUsed, limit: 100, remaining: 100 - previewSearchUsed, resetAt: new Date(Date.now() + 31 * 60 * 60 * 1000).toISOString() },
1735
- historicalConversationQuota: { used: 191, limit: 250, remaining: 59 },
1734
+ memorySearchQuota: { used: previewSearchUsed, limit: 500, remaining: 500 - previewSearchUsed, resetAt: new Date(Date.now() + 31 * 60 * 60 * 1000).toISOString() },
1735
+ historicalConversationQuota: { used: 191, limit: 500, remaining: 309 },
1736
1736
  };
1737
1737
  const sample = {
1738
1738
  client: "codex",
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, deleteMemorySchema, getByContextSchema, keywordsSchema, listFriendsSchema, listToolSpecs, othersSchema, publicMemorySchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, searchUsersSchema, sendFriendRequestSchema, timeRangeSchema, } from "./v1-contract.js";
7
+ import { canonicalToolNames, completeGroupPublicationSchema, createGroupInviteSchema, createGroupSchema, deleteMemorySchema, getByContextSchema, groupContextSchema, joinGroupSchema, keywordsSchema, listFriendsSchema, listToolSpecs, othersSchema, publishBatchToGroupSchema, publishToGroupSchema, prepareGroupPublicationSchema, publicMemorySchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, searchUsersSchema, sendFriendRequestSchema, timeRangeSchema, } 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";
@@ -488,12 +488,20 @@ function inputAnalyticsForTool(canonicalName, args) {
488
488
  limit: numberArg(a, "limit"),
489
489
  };
490
490
  }
491
- case canonicalToolNames.publicMemory: {
491
+ case canonicalToolNames.publicMemory:
492
+ case canonicalToolNames.publishToGroup: {
492
493
  const memoryId = readString(a, "memoryId");
493
494
  return {
494
495
  memory_id_hash: memoryId ? hashText(memoryId) : undefined,
495
496
  };
496
497
  }
498
+ case canonicalToolNames.publishBatchToGroup: {
499
+ const memoryIds = Array.isArray(a.memoryIds) ? a.memoryIds.filter((id) => typeof id === "string") : [];
500
+ return {
501
+ memory_count: memoryIds.length,
502
+ context_id_hash: typeof a.contextId === "string" ? hashText(a.contextId) : undefined,
503
+ };
504
+ }
497
505
  default:
498
506
  return {};
499
507
  }
@@ -944,6 +952,72 @@ class EchoMemApiClient {
944
952
  throw new Error(`get_public_memory failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
945
953
  }
946
954
  }
955
+ async getGroupContext(args) {
956
+ groupContextSchema.parse(args ?? {});
957
+ try {
958
+ const response = await this.axios.get("/api/extension/social/groups/current");
959
+ return response.data;
960
+ }
961
+ catch (error) {
962
+ throw new Error(`get_group_context failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
963
+ }
964
+ }
965
+ async createGroup(args) {
966
+ const parsed = createGroupSchema.parse(args ?? {});
967
+ const response = await this.axios.post("/api/extension/social/groups", parsed);
968
+ return response.data;
969
+ }
970
+ async createGroupInvite(args) {
971
+ const parsed = createGroupInviteSchema.parse(args ?? {});
972
+ const response = await this.axios.post("/api/extension/social/groups/current/invite", parsed);
973
+ return response.data;
974
+ }
975
+ async joinGroup(args) {
976
+ const parsed = joinGroupSchema.parse(args ?? {});
977
+ const response = await this.axios.post("/api/extension/social/groups/join", parsed);
978
+ return response.data;
979
+ }
980
+ async prepareGroupPublication(args) {
981
+ const parsed = prepareGroupPublicationSchema.parse(args ?? {});
982
+ const enc = await this.encState();
983
+ const response = await this.axios.post("/api/extension/social/groups/current/publications/prepare", parsed, { headers: enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : undefined });
984
+ return response.data;
985
+ }
986
+ async completeGroupPublication(args) {
987
+ const parsed = completeGroupPublicationSchema.parse(args ?? {});
988
+ const enc = await this.encState();
989
+ const response = await this.axios.post("/api/extension/social/groups/current/publications/complete", parsed, { headers: enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : undefined });
990
+ return response.data;
991
+ }
992
+ async publishMemoryToGroup(args) {
993
+ const parsed = publishToGroupSchema.parse(args ?? {});
994
+ const enc = await this.encState();
995
+ try {
996
+ const response = await this.axios.post(`/api/extension/social/groups/current/memories/${encodeURIComponent(parsed.memoryId)}/publish`, {}, {
997
+ headers: enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : undefined,
998
+ });
999
+ return response.data;
1000
+ }
1001
+ catch (error) {
1002
+ throw new Error(`publish_memory_to_group failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
1003
+ }
1004
+ }
1005
+ async publishMemoriesToGroup(args) {
1006
+ const parsed = publishBatchToGroupSchema.parse(args ?? {});
1007
+ const enc = await this.encState();
1008
+ try {
1009
+ const response = await this.axios.post("/api/extension/social/groups/current/memories/publish-batch", {
1010
+ memoryIds: parsed.memoryIds,
1011
+ contextId: parsed.contextId,
1012
+ }, {
1013
+ headers: enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : undefined,
1014
+ });
1015
+ return response.data;
1016
+ }
1017
+ catch (error) {
1018
+ throw new Error(`publish_memories_to_group failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
1019
+ }
1020
+ }
947
1021
  }
948
1022
  const SERVER_VERSION = MCP_PACKAGE_VERSION;
949
1023
  class EchoMemMCPServer {
@@ -1121,6 +1195,22 @@ class EchoMemMCPServer {
1121
1195
  return await this.handleOthers(request.params.arguments);
1122
1196
  case canonicalToolNames.publicMemory:
1123
1197
  return await this.handlePublicMemory(request.params.arguments);
1198
+ case canonicalToolNames.groupContext:
1199
+ return await this.handleGroupContext(request.params.arguments);
1200
+ case canonicalToolNames.createGroup:
1201
+ return await this.handleCreateGroup(request.params.arguments);
1202
+ case canonicalToolNames.createGroupInvite:
1203
+ return await this.handleCreateGroupInvite(request.params.arguments);
1204
+ case canonicalToolNames.joinGroup:
1205
+ return await this.handleJoinGroup(request.params.arguments);
1206
+ case canonicalToolNames.prepareGroupPublication:
1207
+ return await this.handlePrepareGroupPublication(request.params.arguments);
1208
+ case canonicalToolNames.completeGroupPublication:
1209
+ return await this.handleCompleteGroupPublication(request.params.arguments);
1210
+ case canonicalToolNames.publishToGroup:
1211
+ return await this.handlePublishToGroup(request.params.arguments);
1212
+ case canonicalToolNames.publishBatchToGroup:
1213
+ return await this.handlePublishBatchToGroup(request.params.arguments);
1124
1214
  case canonicalToolNames.delete:
1125
1215
  return await this.handleDelete(request.params.arguments, rec);
1126
1216
  default:
@@ -1302,7 +1392,7 @@ class EchoMemMCPServer {
1302
1392
  return { content: [{ type: "text", text: "No relevant memories found." }] };
1303
1393
  }
1304
1394
  const formattedResults = memories
1305
- .map((m, idx) => `[Result ${idx + 1}] (Similarity: ${m.similarity_score?.toFixed(3) || "N/A"})
1395
+ .map((m, idx) => `[Result ${idx + 1}] Memory ID: ${m.id || "unknown"} (Similarity: ${m.similarity_score?.toFixed(3) || "N/A"})
1306
1396
  Time: ${m.time} | Location: ${m.location}
1307
1397
  Category: ${m.category} | Object: ${m.object} | Emotion: ${m.emotion}
1308
1398
  Description: ${m.description}
@@ -1504,11 +1594,11 @@ Details: ${m.details || "N/A"}`)
1504
1594
  const memories = payload?.memories ?? [];
1505
1595
  if (!memories.length) {
1506
1596
  const scope = parsed.ownerUserId
1507
- ? ` for friend ${parsed.ownerUserId}`
1597
+ ? ` for peer ${parsed.ownerUserId}`
1508
1598
  : parsed.ownerName
1509
- ? ` for friend ${parsed.ownerName}`
1599
+ ? ` for peer ${parsed.ownerName}`
1510
1600
  : parsed.target
1511
- ? ` for friend ${parsed.target}`
1601
+ ? ` for peer ${parsed.target}`
1512
1602
  : parsed.targetFriendIds?.length
1513
1603
  ? ` for ${parsed.targetFriendIds.length} selected friends`
1514
1604
  : parsed.targetFriendNames?.length
@@ -1521,7 +1611,12 @@ Details: ${m.details || "N/A"}`)
1521
1611
  }
1522
1612
  const metadata = [
1523
1613
  typeof payload?.friendCount === "number" ? `friendCount=${payload.friendCount}` : "",
1524
- typeof payload?.searchedFriendCount === "number" ? `searchedFriendCount=${payload.searchedFriendCount}` : "",
1614
+ typeof payload?.groupPeerCount === "number" ? `groupPeerCount=${payload.groupPeerCount}` : "",
1615
+ typeof payload?.searchedPeerCount === "number"
1616
+ ? `searchedPeerCount=${payload.searchedPeerCount}`
1617
+ : typeof payload?.searchedFriendCount === "number"
1618
+ ? `searchedPeerCount=${payload.searchedFriendCount}`
1619
+ : "",
1525
1620
  typeof payload?.memoryViewsInserted === "number" ? `memoryViewsInserted=${payload.memoryViewsInserted}` : "",
1526
1621
  ].filter(Boolean).join(", ");
1527
1622
  const formattedResults = memories
@@ -1674,6 +1769,168 @@ Details: ${m.details || "N/A"}`;
1674
1769
  content: [{ type: "text", text }],
1675
1770
  };
1676
1771
  }
1772
+ async handleGroupContext(args) {
1773
+ groupContextSchema.parse(args ?? {});
1774
+ const payload = await this.client.getGroupContext(args);
1775
+ const group = isRecord(payload?.group) ? payload.group : null;
1776
+ const participants = Array.isArray(payload?.participants)
1777
+ ? payload.participants.filter(isRecord)
1778
+ : [];
1779
+ const coverage = isRecord(payload?.coverage) ? payload.coverage : {};
1780
+ if (!group) {
1781
+ return {
1782
+ content: [{
1783
+ type: "text",
1784
+ text: "You are not assigned to a company group yet. Ask an EchoMem administrator to add you before using group orientation or group publishing.",
1785
+ }],
1786
+ };
1787
+ }
1788
+ const participantText = participants.map((participant, index) => [
1789
+ `[${index + 1}] ${readString(participant, "displayName") ?? "Unnamed participant"}`,
1790
+ `User ID: ${readString(participant, "userId") ?? "unknown"}`,
1791
+ `Title: ${readString(participant, "title") ?? "Not declared"}`,
1792
+ `Declared responsibility: ${readString(participant, "responsibilitySummary") ?? "Not provided"}`,
1793
+ `Published memories: ${readNumber(participant, "publicMemoryCount") ?? 0}`,
1794
+ ].join("\n")).join("\n\n");
1795
+ const participantCount = readNumber(coverage, "participantCount") ?? participants.length;
1796
+ const coveredCount = readNumber(coverage, "participantsWithPublishedMemories") ?? 0;
1797
+ const totalPublished = readNumber(coverage, "totalPublishedMemories") ?? 0;
1798
+ const text = [
1799
+ `Company group: ${readString(group, "name") ?? "Unnamed group"}`,
1800
+ readString(group, "description") ? `Description: ${readString(group, "description")}` : "",
1801
+ `Coverage: ${coveredCount} of ${participantCount} participants have published memories (${totalPublished} total).`,
1802
+ "",
1803
+ participantText,
1804
+ "",
1805
+ "Declared titles and responsibilities are directory facts. Use search_others_memories for current work evidence, and label suggested contribution areas as inference that should be confirmed with the team.",
1806
+ ].filter(Boolean).join("\n");
1807
+ return { content: [{ type: "text", text }] };
1808
+ }
1809
+ async handlePublishToGroup(args) {
1810
+ const parsed = publishToGroupSchema.parse(args ?? {});
1811
+ const payload = await this.client.publishMemoryToGroup(args);
1812
+ if (payload?.success === false) {
1813
+ throw new Error(`EchoMem API Error: ${payload.error || "publish failed"}`);
1814
+ }
1815
+ const group = isRecord(payload?.group) ? payload.group : {};
1816
+ const groupName = readString(group, "name") ?? "your company group";
1817
+ const memberCount = typeof payload?.searchableByGroupMemberCount === "number"
1818
+ ? payload.searchableByGroupMemberCount
1819
+ : 0;
1820
+ const alreadyPublished = payload?.alreadyPublished === true;
1821
+ return {
1822
+ content: [{
1823
+ type: "text",
1824
+ text: alreadyPublished
1825
+ ? `Memory ${parsed.memoryId} was already published to ${groupName}. It is searchable by up to ${memberCount} other group members.`
1826
+ : `Published memory ${parsed.memoryId} to ${groupName}. It is now searchable by up to ${memberCount} other group members.`,
1827
+ }],
1828
+ };
1829
+ }
1830
+ async handleCreateGroup(args) {
1831
+ const parsed = createGroupSchema.parse(args ?? {});
1832
+ const payload = await this.client.createGroup(parsed);
1833
+ return { content: [{ type: "text", text: `Created company group ${payload?.group?.name ?? parsed.name}. Generate an invite code when you are ready to add teammates.` }] };
1834
+ }
1835
+ async handleCreateGroupInvite(args) {
1836
+ createGroupInviteSchema.parse(args ?? {});
1837
+ const payload = await this.client.createGroupInvite(args);
1838
+ return {
1839
+ content: [{
1840
+ type: "text",
1841
+ text: [
1842
+ `Invite code for ${payload?.group?.name ?? "your group"}: ${payload?.code}`,
1843
+ `Expires: ${payload?.expiresAt}`,
1844
+ `Maximum uses: ${payload?.maxUses}`,
1845
+ "Share this secret code only with intended teammates.",
1846
+ ].join("\n"),
1847
+ }],
1848
+ };
1849
+ }
1850
+ async handleJoinGroup(args) {
1851
+ const parsed = joinGroupSchema.parse(args ?? {});
1852
+ const payload = await this.client.joinGroup(parsed);
1853
+ return {
1854
+ content: [{
1855
+ type: "text",
1856
+ text: payload?.alreadyMember
1857
+ ? `You are already a member of ${payload?.group?.name ?? "this group"}. No memories were published.`
1858
+ : `Joined ${payload?.group?.name ?? "the company group"}. No memories were published. Use prepare_group_publication to review candidates.`,
1859
+ }],
1860
+ };
1861
+ }
1862
+ async handlePrepareGroupPublication(args) {
1863
+ const parsed = prepareGroupPublicationSchema.parse(args ?? {});
1864
+ const payload = await this.client.prepareGroupPublication(parsed);
1865
+ const candidates = Array.isArray(payload?.candidates) ? payload.candidates : [];
1866
+ const formatted = candidates.map((candidate, index) => [
1867
+ `[${index + 1}] Memory ID: ${readString(candidate, "memoryId") ?? "unknown"}`,
1868
+ `Created: ${readString(candidate, "createdAt") ?? "unknown"}`,
1869
+ `Category: ${readString(candidate, "category") ?? "unknown"}`,
1870
+ `Description: ${readString(candidate, "description") ?? ""}`,
1871
+ readString(candidate, "details") ? `Details: ${readString(candidate, "details")}` : "",
1872
+ `Already published: ${candidate.alreadyPublished === true ? "yes" : "no"}`,
1873
+ `Exact content duplicate: ${candidate.exactContentDuplicate === true ? "yes" : "no"}`,
1874
+ ].filter(Boolean).join("\n")).join("\n\n");
1875
+ return {
1876
+ content: [{
1877
+ type: "text",
1878
+ text: [
1879
+ `Prepared publication scan ${payload?.scanId} for ${payload?.group?.name ?? "your group"}.`,
1880
+ `Window: ${payload?.windowStart ?? "context start"} → ${payload?.windowEnd}`,
1881
+ `Candidates: ${candidates.length}. Nothing has been published.`,
1882
+ "",
1883
+ formatted,
1884
+ "",
1885
+ "Select exact memory IDs matching the user's instruction, show the preview, and ask for confirmation before calling complete_group_publication.",
1886
+ ].join("\n"),
1887
+ }],
1888
+ };
1889
+ }
1890
+ async handleCompleteGroupPublication(args) {
1891
+ const parsed = completeGroupPublicationSchema.parse(args ?? {});
1892
+ const payload = await this.client.completeGroupPublication(parsed);
1893
+ const memories = Array.isArray(payload?.memories) ? payload.memories : [];
1894
+ const published = memories.filter((memory) => memory.alreadyPublished !== true && memory.skippedDuplicate !== true).length;
1895
+ const duplicates = memories.filter((memory) => memory.skippedDuplicate === true).length;
1896
+ return {
1897
+ content: [{
1898
+ type: "text",
1899
+ text: [
1900
+ `Completed publication scan ${parsed.scanId}.`,
1901
+ `Newly published: ${published}.`,
1902
+ `Exact duplicates skipped: ${duplicates}.`,
1903
+ `Selected memory IDs: ${parsed.memoryIds.join(", ") || "(none)"}.`,
1904
+ "The scan cursor advanced; encrypted originals and global public settings were unchanged.",
1905
+ ].join("\n"),
1906
+ }],
1907
+ };
1908
+ }
1909
+ async handlePublishBatchToGroup(args) {
1910
+ const parsed = publishBatchToGroupSchema.parse(args ?? {});
1911
+ const payload = await this.client.publishMemoriesToGroup(parsed);
1912
+ if (payload?.success === false) {
1913
+ throw new Error(`EchoMem API Error: ${payload.error || "batch publish failed"}`);
1914
+ }
1915
+ const group = isRecord(payload?.group) ? payload.group : {};
1916
+ const groupName = readString(group, "name") ?? "your company group";
1917
+ const publishedCount = typeof payload?.publishedCount === "number" ? payload.publishedCount : 0;
1918
+ const alreadyPublishedCount = typeof payload?.alreadyPublishedCount === "number"
1919
+ ? payload.alreadyPublishedCount
1920
+ : 0;
1921
+ return {
1922
+ content: [{
1923
+ type: "text",
1924
+ text: [
1925
+ `Published ${publishedCount} memory snapshots to ${groupName}.`,
1926
+ alreadyPublishedCount ? `${alreadyPublishedCount} were already published and were refreshed.` : "",
1927
+ `Selected memory IDs: ${parsed.memoryIds.join(", ")}`,
1928
+ parsed.selectionReason ? `Selection reason: ${parsed.selectionReason}` : "",
1929
+ "The encrypted originals and their global public settings were not changed.",
1930
+ ].filter(Boolean).join("\n"),
1931
+ }],
1932
+ };
1933
+ }
1677
1934
  async handleDelete(args, rec) {
1678
1935
  const parsed = deleteMemorySchema.parse(args ?? {});
1679
1936
  if (rec) {
package/dist/migrate.js CHANGED
@@ -4,9 +4,10 @@
4
4
  *
5
5
  * Why this lives in the bridge (client-side): the session logs exist ONLY on the user's machine
6
6
  * (~/.codex/sessions, ~/.claude/projects). The bridge discovers each session, assembles it into a
7
- * text-turn-only `## ` transcript, and feeds it to the EXISTING durable import queue
8
- * (the same one the extension uses): POST /api/extension/import-sessions creates one import_jobs row per session, then the bridge
9
- * POSTs each transcript to /api/extension/import-jobs/{id}/run. The web dashboard polls
7
+ * text-turn-only `## ` transcript, and feeds it to the shared durable import queue
8
+ * through MCP-owned policy entrypoints. POST /api/extension/mcp/historical-import-sessions
9
+ * creates one import_jobs row per session, then the bridge POSTs each transcript to
10
+ * /api/extension/mcp/historical-import-jobs/{id}/run. The web dashboard polls
10
11
  * GET /import-sessions/{id} for live progress — the bridge owns local-file access, the server owns the
11
12
  * queue + status. (Headless CLI also works; it just shows progress in the terminal.)
12
13
  *
@@ -1353,7 +1354,7 @@ async function createImportSession(client, sessions, bareId, userTz, signal) {
1353
1354
  sourceDate: s.firstTs,
1354
1355
  userTz,
1355
1356
  }));
1356
- const res = await client.post("/api/extension/import-sessions", { items }, signal ? { signal } : undefined);
1357
+ const res = await client.post("/api/extension/mcp/historical-import-sessions", { items }, signal ? { signal } : undefined);
1357
1358
  const data = res.data || {};
1358
1359
  const serverConcurrency = Number(data.session?.concurrency);
1359
1360
  return {
@@ -1378,7 +1379,7 @@ async function runImportJob(client, jobId, s, userTz, encKey) {
1378
1379
  const cfg = encKey ? { headers: { "X-Encryption-Key": encKey } } : undefined;
1379
1380
  for (let attempt = 0;; attempt++) {
1380
1381
  try {
1381
- const res = await client.post(`/api/extension/import-jobs/${jobId}/run`, body, cfg);
1382
+ const res = await client.post(`/api/extension/mcp/historical-import-jobs/${jobId}/run`, body, cfg);
1382
1383
  const data = res.data || {};
1383
1384
  if (data.claimed === false) {
1384
1385
  // Already completed on a prior run → idempotent, not an error; otherwise it didn't start.
@@ -28,6 +28,8 @@ export const MCP_SERVER_INSTRUCTIONS = [
28
28
  `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.`,
29
29
  "Use echomem_update_status to check whether npm has a newer bridge; it is cached and non-blocking during normal tool listing.",
30
30
  "Do not auto-update on every MCP startup; this bridge is intentionally stable between explicit updates.",
31
+ "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-write preview before complete_group_publication.",
32
+ "Never complete a group publication without explicit confirmation of the prepared candidate set, and never store or log an echo_grp_ invite code.",
31
33
  ].join(" ");
32
34
  export function withMcpVersion(description) {
33
35
  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.`;
@@ -131,7 +131,10 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
131
131
  }
132
132
  async function getJson(path) {
133
133
  var sep = path.indexOf("?") >= 0 ? "&" : "?";
134
- var res = await fetch(path + sep + "nonce=" + encodeURIComponent(nonce), { credentials: "omit" });
134
+ var res = await fetch(path + sep + "nonce=" + encodeURIComponent(nonce), {
135
+ credentials: "omit",
136
+ cache: "no-store"
137
+ });
135
138
  if (!res.ok) throw new Error(await res.text() || ("HTTP " + res.status));
136
139
  return await res.json();
137
140
  }
@@ -627,8 +627,8 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
627
627
  return Math.max(0, Math.floor(quota.remaining));
628
628
  }
629
629
  var plan = String(billingStatus && billingStatus.plan || "").toLowerCase();
630
- if (plan === "power" || plan === "enterprise") return 500;
631
- if (paidRecallPlan(plan)) return 250;
630
+ if (plan === "power" || plan === "enterprise") return 2000;
631
+ if (paidRecallPlan(plan)) return 500;
632
632
  return 100;
633
633
  }
634
634
  function selectedSessionKeyList() {
@@ -912,7 +912,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
912
912
  homeName: "Power",
913
913
  promise: "More room. More agents. Much more Echo.",
914
914
  price: "$100",
915
- cadence: "per month · cancel anytime",
915
+ cadence: "per month · 14-day trial",
916
916
  sticker: "/hud-assets/echo-pricing-power-sticker.png",
917
917
  stickerAlt: "Power Echo arrives with a gold key and a crew of notebook helpers.",
918
918
  features: ["2,000 past chats", "agent-heavy memory", "2,000 context returns / week"]
@@ -923,7 +923,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
923
923
  homeName: "Pro",
924
924
  promise: "A room for the work you return to every day.",
925
925
  price: "$20",
926
- cadence: "per month · cancel anytime",
926
+ cadence: "per month · 14-day trial",
927
927
  sticker: "/hud-assets/echo-pricing-pro-sticker.png",
928
928
  stickerAlt: "Pro Echo organizes tabbed notebooks and a daily refresh control.",
929
929
  popular: true,
@@ -960,6 +960,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
960
960
  return '<li>' + setupIcon("check") + '<span>' + esc(feature) + '</span></li>';
961
961
  }).join("");
962
962
  var chooseId = plan.id === "free" ? "chooseFreePlan" : (plan.id === "power" ? "choosePowerPlan" : "chooseProPlan");
963
+ var chooseLabel = plan.id === "free" ? "Take " + plan.homeName + " home" : "Start 14-day trial";
963
964
  var discovery = discovered > 0
964
965
  ? '<p class="mvpPricingDiscovery">Echo found <strong>' + esc(number(discovered)) + '</strong> new coding session' + (discovered === 1 ? "" : "s") + ' on this Mac.</p>'
965
966
  : "";
@@ -980,7 +981,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
980
981
  '<div class="mvpPlanPrice"><strong>' + esc(plan.price) + '</strong><span>' + esc(plan.cadence) + '</span></div>' +
981
982
  '<p class="mvpPacksLabel">Echo packs</p>' +
982
983
  '<ul class="mvpPlanFacts">' + facts + '</ul>' +
983
- '<button type="button" class="primary mvpPlanCta" id="' + chooseId + '">Take ' + esc(plan.homeName) + ' home <span aria-hidden="true">→</span></button>' +
984
+ '<button type="button" class="primary mvpPlanCta" id="' + chooseId + '">' + esc(chooseLabel) + ' <span aria-hidden="true">→</span></button>' +
984
985
  '</div>' +
985
986
  '</article>' +
986
987
  '<p class="mvpPlanStatus" id="setupPlanStatus" role="status"></p>' +
@@ -1022,7 +1023,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1022
1023
  if (!canExtract) {
1023
1024
  slot.innerHTML = "";
1024
1025
  slot.classList.add("is-hidden");
1025
- if (readySettings) readySettings.classList.remove("is-plan-open");
1026
+ if (readySettings) readySettings.classList.remove("is-plan-open", "is-checkout-pending");
1026
1027
  return;
1027
1028
  }
1028
1029
  slot.classList.remove("is-hidden");
@@ -1035,7 +1036,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1035
1036
  renderDashboard();
1036
1037
  return;
1037
1038
  }
1038
- if (readySettings) readySettings.classList.remove("is-plan-open");
1039
+ if (readySettings) readySettings.classList.remove("is-plan-open", "is-checkout-pending");
1039
1040
  rememberSetupPlanChoice("");
1040
1041
  setupPlanChoice = "paid";
1041
1042
  var planLabel = plan.charAt(0).toUpperCase() + plan.slice(1);
@@ -1046,7 +1047,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1046
1047
  ? paidHistory.used + " / " + paidHistory.limit + " bulk history imports used."
1047
1048
  : "Your bulk history import and weekly new-chat allowance are ready.";
1048
1049
  if (checkoutJustConfirmed) document.title = "Echo plan active — continue setup";
1049
- slot.innerHTML = '<div class="setupPlanConfirmed' + (checkoutJustConfirmed ? ' is-checkout-confirmed' : '') + '"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>' + (checkoutJustConfirmed ? 'Payment confirmed. ' : '') + esc(planLabel) + ' Echo</strong><span>' + esc(checkoutJustConfirmed ? "Your plan is ready. Continue below—checkout never needed access to this localhost page." : paidUsage) + '</span></span><button type="button" class="textButton" id="changeActivePlan">Change</button></div>';
1050
+ slot.innerHTML = '<div class="setupPlanConfirmed' + (checkoutJustConfirmed ? ' is-checkout-confirmed' : '') + '"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>' + (checkoutJustConfirmed ? 'Your Echo is ready. ' : '') + esc(planLabel) + ' Echo</strong><span>' + esc(checkoutJustConfirmed ? "Continue below." : paidUsage) + '</span></span><button type="button" class="textButton" id="changeActivePlan">Change</button></div>';
1050
1051
  var changeActivePlan = document.getElementById("changeActivePlan");
1051
1052
  if (changeActivePlan) changeActivePlan.onclick = openHostedPlanOptions;
1052
1053
  renderSessionSelection(true);
@@ -1054,8 +1055,8 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1054
1055
  return;
1055
1056
  }
1056
1057
  if (setupPlanChoice === "free") {
1057
- if (readySettings) readySettings.classList.remove("is-plan-open");
1058
- slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>Original Echo</strong><span>100 bulk history imports, 25K new-chat tokens each week, and 10 memory searches each week.</span></span><button type="button" class="textButton" id="changeSetupPlan">Change</button></div>';
1058
+ if (readySettings) readySettings.classList.remove("is-plan-open", "is-checkout-pending");
1059
+ slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>Original Echo</strong><span>100 bulk history imports, 25K new-chat tokens each week, and 100 memory searches each week.</span></span><button type="button" class="textButton" id="changeSetupPlan">Change</button></div>';
1059
1060
  var changeBtn = document.getElementById("changeSetupPlan");
1060
1061
  if (changeBtn) changeBtn.onclick = function () {
1061
1062
  rememberSetupPlanChoice("");
@@ -1068,11 +1069,14 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1068
1069
  ? "power"
1069
1070
  : (setupPlanChoice === "pro" || billingActivationPendingPlan === "pro" ? "pro" : "");
1070
1071
  if (pendingPaidPlan) {
1071
- if (readySettings) readySettings.classList.remove("is-plan-open");
1072
+ if (readySettings) {
1073
+ readySettings.classList.remove("is-plan-open");
1074
+ readySettings.classList.add("is-checkout-pending");
1075
+ }
1072
1076
  var selectedPaidPlan = pendingPaidPlan === "power" ? "Power" : "Pro";
1073
1077
  slot.innerHTML =
1074
1078
  '<section class="setupPlanChoice setupPlanCheckout" aria-labelledby="setupPlanHeading">' +
1075
- '<div class="setupPlanIntro"><h3 id="setupPlanHeading">' + selectedPaidPlan + ' checkout is open.</h3><p>Finish in the secure Stripe tab. This page will notice automatically when your plan becomes active.</p></div>' +
1079
+ '<div class="setupPlanIntro"><h3 id="setupPlanHeading">Start your 14-day trial.</h3><p>Finish in the secure Stripe tab, then return here. Echo will update automatically.</p></div>' +
1076
1080
  '<button type="button" class="primary" id="continueSelectedPaidPlan">Reopen secure checkout</button>' +
1077
1081
  '<div class="setupPlanFoot"><button type="button" class="textButton" id="changeSelectedPaidPlan">Choose another Echo</button><span id="setupPlanStatus" role="status"></span></div>' +
1078
1082
  '</section>';
@@ -1090,7 +1094,10 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1090
1094
  renderSessionSelection(true);
1091
1095
  return;
1092
1096
  }
1093
- if (readySettings) readySettings.classList.add("is-plan-open");
1097
+ if (readySettings) {
1098
+ readySettings.classList.remove("is-checkout-pending");
1099
+ readySettings.classList.add("is-plan-open");
1100
+ }
1094
1101
  slot.innerHTML = renderPlanHabitat();
1095
1102
  bindPlanHabitat();
1096
1103
  if (migrateBtn) migrateBtn.disabled = true;
@@ -807,6 +807,95 @@ export const SETUP_PAGE_STYLES_MVP = String.raw `
807
807
  padding: 0 8px;
808
808
  font-size: 11px;
809
809
  }
810
+ .extractionReady .readySettings.is-checkout-pending {
811
+ align-items: flex-start;
812
+ }
813
+ .extractionReady .readySettings.is-checkout-pending .readyPlanSetting {
814
+ display: block;
815
+ width: min(680px, calc(100vw - 260px));
816
+ overflow: visible;
817
+ border: 0;
818
+ border-radius: 0;
819
+ background: transparent;
820
+ padding: 0;
821
+ box-shadow: none;
822
+ backdrop-filter: none;
823
+ }
824
+ .extractionReady .readySettings.is-checkout-pending .readyPlanSetting > .readySettingLabel {
825
+ position: absolute;
826
+ width: 1px;
827
+ height: 1px;
828
+ overflow: hidden;
829
+ clip: rect(0 0 0 0);
830
+ clip-path: inset(50%);
831
+ white-space: nowrap;
832
+ }
833
+ .extractionReady .readySettings.is-checkout-pending .setupPlanSlot {
834
+ margin: 0;
835
+ }
836
+ .extractionReady .setupPlanCheckout {
837
+ display: grid;
838
+ grid-template-columns: minmax(0, 1fr) auto;
839
+ grid-template-rows: auto auto;
840
+ column-gap: 18px;
841
+ row-gap: 2px;
842
+ align-items: center;
843
+ margin: 0;
844
+ overflow: hidden;
845
+ border: 1px solid rgba(26,58,143,0.16);
846
+ border-radius: 18px;
847
+ background: rgba(255,255,255,0.94);
848
+ padding: 13px 15px 12px 18px;
849
+ box-shadow: 0 12px 32px rgba(26,58,143,0.12);
850
+ backdrop-filter: blur(16px);
851
+ }
852
+ .extractionReady .setupPlanCheckout::before {
853
+ display: block;
854
+ position: absolute;
855
+ inset: 0 auto 0 0;
856
+ width: 4px;
857
+ height: auto;
858
+ background: var(--echo-ink-primary);
859
+ opacity: 1;
860
+ }
861
+ .extractionReady .setupPlanCheckout .setupPlanIntro {
862
+ min-width: 0;
863
+ padding: 0;
864
+ }
865
+ .extractionReady .setupPlanCheckout .setupPlanIntro h3 {
866
+ font-size: 16px;
867
+ line-height: 1.2;
868
+ letter-spacing: -0.01em;
869
+ }
870
+ .extractionReady .setupPlanCheckout .setupPlanIntro p {
871
+ margin-top: 3px;
872
+ font-size: 11px;
873
+ line-height: 1.35;
874
+ }
875
+ .extractionReady .setupPlanCheckout > .primary {
876
+ grid-column: 2;
877
+ grid-row: 1 / span 2;
878
+ min-height: 42px;
879
+ border-radius: 999px;
880
+ padding: 0 18px;
881
+ white-space: nowrap;
882
+ box-shadow: 0 7px 18px rgba(26,58,143,0.18);
883
+ }
884
+ .extractionReady .setupPlanCheckout .setupPlanFoot {
885
+ justify-content: flex-start;
886
+ gap: 8px;
887
+ margin-top: 1px;
888
+ }
889
+ .extractionReady .setupPlanCheckout .setupPlanFoot .textButton {
890
+ min-height: 0;
891
+ padding: 2px 0;
892
+ color: var(--echo-ink-mute);
893
+ font-size: 10px;
894
+ font-weight: 700;
895
+ }
896
+ .extractionReady .setupPlanCheckout .setupPlanFoot span {
897
+ font-size: 10px;
898
+ }
810
899
  .extractionReady .readyFoot {
811
900
  width: min(620px, 100%);
812
901
  margin: 12px auto 0;
@@ -954,6 +1043,20 @@ export const SETUP_PAGE_STYLES_MVP = String.raw `
954
1043
  .mvpHabitatDetails { padding: 28px 24px 34px; }
955
1044
  .mvpHabitatEcho { width: min(66%, 360px); }
956
1045
  .extractionReady .readySettings { flex-direction: column; gap: 9px; }
1046
+ body.readyMode:has(.readySettings.is-checkout-pending) main {
1047
+ padding-top: 190px;
1048
+ }
1049
+ .extractionReady .readySettings.is-checkout-pending .readyAccountSetting { display: none; }
1050
+ .extractionReady .readySettings.is-checkout-pending .readyPlanSetting { width: 100%; }
1051
+ .extractionReady .setupPlanCheckout {
1052
+ grid-template-columns: 1fr;
1053
+ gap: 9px;
1054
+ }
1055
+ .extractionReady .setupPlanCheckout > .primary {
1056
+ grid-column: 1;
1057
+ grid-row: auto;
1058
+ width: 100%;
1059
+ }
957
1060
  .exHero.exHeroActive { grid-template-columns: 1fr; }
958
1061
  .exHeroActive .exPlate { min-height: 440px; }
959
1062
  }
@@ -339,22 +339,22 @@ export function renderSetupPreviewBootstrap(state) {
339
339
  if (state === "extract-pro-active")
340
340
  return `${watermark}${extractionPreviewBootstrap({
341
341
  plan: "pro", paid: true, trialAvailable: false, trialUsed: true,
342
- subscriptionStatus: "active", quotaLimit: 250, quotaRemaining: 250,
342
+ subscriptionStatus: "active", quotaLimit: 500, quotaRemaining: 500,
343
343
  })}`;
344
344
  if (state === "extract-power-active")
345
345
  return `${watermark}${extractionPreviewBootstrap({
346
346
  plan: "power", paid: true, trialAvailable: false, trialUsed: true,
347
- subscriptionStatus: "active", quotaLimit: 500, quotaRemaining: 500,
347
+ subscriptionStatus: "active", quotaLimit: 2_000, quotaRemaining: 2_000,
348
348
  })}`;
349
349
  if (state === "extract-team-active")
350
350
  return `${watermark}${extractionPreviewBootstrap({
351
351
  plan: "team", paid: true, trialAvailable: false, trialUsed: true,
352
- subscriptionStatus: "active", quotaLimit: 250, quotaRemaining: 250,
352
+ subscriptionStatus: "active", quotaLimit: 500, quotaRemaining: 500,
353
353
  })}`;
354
354
  if (state === "extract-enterprise-active")
355
355
  return `${watermark}${extractionPreviewBootstrap({
356
356
  plan: "enterprise", paid: true, trialAvailable: false, trialUsed: true,
357
- subscriptionStatus: "active", quotaLimit: 500, quotaRemaining: 500,
357
+ subscriptionStatus: "active", quotaLimit: 2_000, quotaRemaining: 2_000,
358
358
  })}`;
359
359
  if (state === "extract-unknown")
360
360
  return `${watermark}${extractionPreviewBootstrap({
package/dist/setup.js CHANGED
@@ -391,6 +391,15 @@ function echomemGuidanceBlock() {
391
391
  "- Before re-deriving past decisions, project context, or anything the user likely settled before: call `search_memories` first.",
392
392
  "- When meaningful work wraps up (a decision, a fix, a milestone) or the user asks to remember something: call `save_conversation`.",
393
393
  "- 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.",
394
+ "",
395
+ "### Company group memory",
396
+ "- Use `get_group_context` when the user asks who is in their company group, what teammates are responsible for, or what work is already covered. Treat declared participant fields as facts and published-memory conclusions as evidence or inference.",
397
+ "- A group publication is separate from a globally public memory: publishing to a group creates a group-scoped snapshot and must not change the encrypted original or its global `is_public` setting.",
398
+ "- If a user asks to create a group, call `create_memory_group`; if they ask for a code to share, call `create_group_invite` and return the secret invite code only to that user. Never save the invite code to memory or include it in logs, analytics, summaries, or unrelated output.",
399
+ "- If a user supplies an `echo_grp_...` code and explicitly asks to join, call `join_memory_group`. Joining never authorizes publishing by itself and must not move a user out of another group.",
400
+ "- For requests such as “prepare my recent work memories,” “upload work from this ticket,” or “publish work since my last sync,” call `prepare_group_publication` first. This is a no-write preview. For encrypted accounts, tell the user to run `echomem-mcp unlock` locally if the tool reports that the key is required.",
401
+ "- After preparing, select only exact candidate memory IDs that match the user's stated scope, exclude already-published or exact-content duplicates, summarize what would be shared, and ask the user to confirm. Do not call `complete_group_publication` until the user explicitly confirms that preview.",
402
+ "- On confirmation, call `complete_group_publication` with the exact `scanId`, selected memory IDs, and `confirmed: true`. An explicit request to join and upload still requires the publication preview and confirmation after joining.",
394
403
  "- If the user asks to show, reopen, restart, or bring back the EchoMem HUD (the context-health overlay), run the shell command `echomem-hud app --client auto`.",
395
404
  "- If the user wants the HUD to come back after a computer restart, run the shell command `echomem-hud autostart on --client auto`.",
396
405
  AGENTS_MD_END,
@@ -1716,6 +1725,7 @@ export function startCallbackServer(opts = {}) {
1716
1725
  return;
1717
1726
  }
1718
1727
  if (route === "/billing-status" && req.method === "GET") {
1728
+ res.setHeader("Cache-Control", "no-store, max-age=0");
1719
1729
  if (!checkNonce(url.searchParams.get("nonce") || undefined))
1720
1730
  return void text(res, 403, "bad nonce");
1721
1731
  if (requiresReportConsent && !reportConsentGranted) {
@@ -10,6 +10,14 @@ export const canonicalToolNames = {
10
10
  sendFriendRequest: "send_friend_request",
11
11
  others: "search_others_memories",
12
12
  publicMemory: "get_public_memory",
13
+ groupContext: "get_group_context",
14
+ createGroup: "create_memory_group",
15
+ createGroupInvite: "create_group_invite",
16
+ joinGroup: "join_memory_group",
17
+ prepareGroupPublication: "prepare_group_publication",
18
+ completeGroupPublication: "complete_group_publication",
19
+ publishToGroup: "publish_memory_to_group",
20
+ publishBatchToGroup: "publish_memories_to_group",
13
21
  report: "echomem_usage_report",
14
22
  updateStatus: "echomem_update_status",
15
23
  contextHealth: "echo_context_health",
@@ -59,9 +67,17 @@ export const timeRangeSchema = z.object({
59
67
  endDate: z.string(),
60
68
  limit: z.number().optional().default(50),
61
69
  });
70
+ const keywordListSchema = z.preprocess((value) => {
71
+ if (typeof value !== "string")
72
+ return value;
73
+ return value
74
+ .split(",")
75
+ .map((keyword) => keyword.trim())
76
+ .filter(Boolean);
77
+ }, z.array(z.string().trim().min(1)).min(1).max(50));
62
78
  export const keywordsSchema = z.object({
63
79
  ...triggerMetadataSchema,
64
- keywords: z.array(z.string()),
80
+ keywords: keywordListSchema,
65
81
  limit: z.number().optional().default(10),
66
82
  });
67
83
  export const listFriendsSchema = z.object({
@@ -94,6 +110,56 @@ export const publicMemorySchema = z.object({
94
110
  ...triggerMetadataSchema,
95
111
  memoryId: z.string().min(1),
96
112
  });
113
+ export const groupContextSchema = z.object({
114
+ ...triggerMetadataSchema,
115
+ });
116
+ export const createGroupSchema = z.object({
117
+ ...triggerMetadataSchema,
118
+ name: z.string().min(1).max(120),
119
+ description: z.string().max(1000).optional(),
120
+ displayName: z.string().min(1).max(120),
121
+ title: z.string().max(160).optional(),
122
+ responsibilitySummary: z.string().max(1000).optional(),
123
+ });
124
+ export const createGroupInviteSchema = z.object({
125
+ ...triggerMetadataSchema,
126
+ expiresInDays: z.number().int().min(1).max(30).optional(),
127
+ maxUses: z.number().int().min(1).max(100).optional(),
128
+ });
129
+ export const joinGroupSchema = z.object({
130
+ ...triggerMetadataSchema,
131
+ code: z.string().min(30),
132
+ displayName: z.string().min(1).max(120),
133
+ title: z.string().max(160).optional(),
134
+ responsibilitySummary: z.string().max(1000).optional(),
135
+ });
136
+ export const prepareGroupPublicationSchema = z.object({
137
+ ...triggerMetadataSchema,
138
+ scope: z.enum(["bootstrap", "since_last_scan", "context", "time_range"]).default("since_last_scan"),
139
+ contextId: z.string().min(1).optional(),
140
+ startAt: z.string().optional(),
141
+ endAt: z.string().optional(),
142
+ lookbackDays: z.number().int().min(1).max(365).optional(),
143
+ limit: z.number().int().min(1).max(50).optional(),
144
+ instruction: z.string().max(500).optional(),
145
+ });
146
+ export const completeGroupPublicationSchema = z.object({
147
+ ...triggerMetadataSchema,
148
+ scanId: z.string().min(1),
149
+ memoryIds: z.array(z.string().min(1)).max(50),
150
+ confirmed: z.literal(true),
151
+ selectionReason: z.string().max(500).optional(),
152
+ });
153
+ export const publishToGroupSchema = z.object({
154
+ ...triggerMetadataSchema,
155
+ memoryId: z.string().min(1),
156
+ });
157
+ export const publishBatchToGroupSchema = z.object({
158
+ ...triggerMetadataSchema,
159
+ memoryIds: z.array(z.string().min(1)).min(1).max(50),
160
+ contextId: z.string().min(1).optional(),
161
+ selectionReason: z.string().max(500).optional(),
162
+ });
97
163
  export const deleteMemorySchema = z.object({
98
164
  memoryId: z.string().min(1),
99
165
  confirmed: z.boolean().optional().default(false),
@@ -108,7 +174,7 @@ export function listToolSpecs(opts = {}) {
108
174
  const currentTime = new Date().toISOString();
109
175
  const map = opts.map?.trim();
110
176
  const updateNotice = opts.updateNotice?.trim();
111
- const recallPlanNote = "Available on every plan: Free includes 10 searches each week, Pro includes 100, and Power includes 250.";
177
+ const recallPlanNote = "Available on every plan: Free includes 100 searches each week, Pro includes 500, and Power includes 2,000.";
112
178
  const searchBillingReplyInstruction = "If search returns an ACTION REQUIRED subscription message, tell the user to start their trial or subscription and include the exact URL from that result verbatim. Do not respond only with \"connect\" or \"upgrade\".";
113
179
  const updateSection = updateNotice ? `\n\nUPDATE NOTICE: ${updateNotice}` : "";
114
180
  const mapSection = map
@@ -204,11 +270,27 @@ export function listToolSpecs(opts = {}) {
204
270
  },
205
271
  {
206
272
  name: canonicalToolNames.keywords,
207
- description: `Search memories based on keywords in keys field. ${recallPlanNote}`,
273
+ description: `Search memories based on keywords in keys field. Pass keywords as valid JSON: preferably an array of quoted strings, for example {"keywords":["flow-lab","flow.html","Rive"],"limit":8}. A comma-separated JSON string is also accepted as a compatibility fallback. Never emit bare comma-separated tokens. ${recallPlanNote}`,
208
274
  inputSchema: {
209
275
  type: "object",
210
276
  properties: {
211
- keywords: { type: "array", items: { type: "string" } },
277
+ keywords: {
278
+ oneOf: [
279
+ {
280
+ type: "array",
281
+ minItems: 1,
282
+ maxItems: 50,
283
+ items: { type: "string", minLength: 1 },
284
+ },
285
+ {
286
+ type: "string",
287
+ minLength: 1,
288
+ description: "Compatibility form: one comma-separated JSON string, such as \"flow-lab, flow.html, Rive\".",
289
+ },
290
+ ],
291
+ description: "Use a JSON array of quoted strings. Do not pass unquoted comma-separated tokens.",
292
+ examples: [["flow-lab", "flow.html", "Rive"]],
293
+ },
212
294
  limit: { type: "number", default: 10 },
213
295
  triggerMessage: {
214
296
  type: "string",
@@ -274,7 +356,7 @@ export function listToolSpecs(opts = {}) {
274
356
  },
275
357
  {
276
358
  name: canonicalToolNames.others,
277
- description: "Search accepted friends' public memories. Returned memories are recorded in the existing memory_views pipeline for the memory owners.",
359
+ 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.",
278
360
  inputSchema: {
279
361
  type: "object",
280
362
  properties: {
@@ -282,25 +364,25 @@ export function listToolSpecs(opts = {}) {
282
364
  limit: { type: "number", default: 10 },
283
365
  target: {
284
366
  type: "string",
285
- description: "Accepted-friend user id or exact display name/username. Prefer this for @Name asks.",
367
+ description: "Accessible friend or group-member user id or exact display name. Prefer this for @Name asks.",
286
368
  },
287
369
  ownerUserId: {
288
370
  type: "string",
289
- description: "Optional accepted-friend user id to scope the search to one friend.",
371
+ description: "Optional accessible user id to scope the search to one person.",
290
372
  },
291
373
  ownerName: {
292
374
  type: "string",
293
- description: "Optional accepted-friend display name/username to scope the search to one friend.",
375
+ description: "Optional accessible friend or group-member display name to scope the search.",
294
376
  },
295
377
  targetFriendIds: {
296
378
  type: "array",
297
379
  items: { type: "string" },
298
- description: "Optional accepted-friend user ids to scope the search.",
380
+ description: "Legacy field for optional accessible friend or group-member user ids.",
299
381
  },
300
382
  targetFriendNames: {
301
383
  type: "array",
302
384
  items: { type: "string" },
303
- description: "Optional accepted-friend display names/usernames to scope the search.",
385
+ description: "Legacy field for optional accessible friend or group-member display names.",
304
386
  },
305
387
  recordAccess: {
306
388
  type: "boolean",
@@ -319,7 +401,7 @@ export function listToolSpecs(opts = {}) {
319
401
  },
320
402
  {
321
403
  name: canonicalToolNames.publicMemory,
322
- description: "Fetch one accepted friend's public memory by id. If the caller is not the owner, EchoMem records the access in the existing memory_views pipeline.",
404
+ description: "Fetch one public memory by id when its owner is an accepted friend or shares your company group. If the caller is not the owner, EchoMem records the access in memory_views.",
323
405
  inputSchema: {
324
406
  type: "object",
325
407
  properties: {
@@ -336,6 +418,139 @@ export function listToolSpecs(opts = {}) {
336
418
  required: ["memoryId"],
337
419
  },
338
420
  },
421
+ {
422
+ name: canonicalToolNames.groupContext,
423
+ 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.",
424
+ inputSchema: {
425
+ type: "object",
426
+ properties: {
427
+ triggerMessage: {
428
+ type: "string",
429
+ description: "Optional user message that caused this group-orientation lookup.",
430
+ },
431
+ triggerMessageRole: { type: "string", default: "user" },
432
+ },
433
+ },
434
+ },
435
+ {
436
+ name: canonicalToolNames.createGroup,
437
+ description: "Create one company memory group for the current user. The MVP allows at most one group per user. Call only after the user explicitly asks to create a group.",
438
+ inputSchema: {
439
+ type: "object",
440
+ properties: {
441
+ name: { type: "string" },
442
+ description: { type: "string" },
443
+ displayName: { type: "string" },
444
+ title: { type: "string" },
445
+ responsibilitySummary: { type: "string" },
446
+ },
447
+ required: ["name", "displayName"],
448
+ },
449
+ },
450
+ {
451
+ name: canonicalToolNames.createGroupInvite,
452
+ description: "Generate a secret, expiring invite code for the current group. Only the group creator may call this in the MVP. Return the code only to the user so they can share it intentionally; never log it.",
453
+ inputSchema: {
454
+ type: "object",
455
+ properties: {
456
+ expiresInDays: { type: "number", default: 7 },
457
+ maxUses: { type: "number", default: 20 },
458
+ },
459
+ },
460
+ },
461
+ {
462
+ name: canonicalToolNames.joinGroup,
463
+ description: "Join a company memory group using an invite code after the user explicitly asks to join. Joining never publishes memories; call prepare_group_publication separately.",
464
+ inputSchema: {
465
+ type: "object",
466
+ properties: {
467
+ code: { type: "string" },
468
+ displayName: { type: "string" },
469
+ title: { type: "string" },
470
+ responsibilitySummary: { type: "string" },
471
+ },
472
+ required: ["code", "displayName"],
473
+ },
474
+ },
475
+ {
476
+ name: canonicalToolNames.prepareGroupPublication,
477
+ description: "Prepare a no-write preview of up to 50 owned memories for group publication. For encrypted accounts the local bridge must be unlocked. The host agent selects exact work-related memory ids from the returned candidates and asks the user to confirm; this tool never publishes.",
478
+ inputSchema: {
479
+ type: "object",
480
+ properties: {
481
+ scope: { type: "string", enum: ["bootstrap", "since_last_scan", "context", "time_range"], default: "since_last_scan" },
482
+ contextId: { type: "string" },
483
+ startAt: { type: "string" },
484
+ endAt: { type: "string" },
485
+ lookbackDays: { type: "number", default: 30 },
486
+ limit: { type: "number", default: 50 },
487
+ instruction: { type: "string", default: "work-related" },
488
+ },
489
+ },
490
+ },
491
+ {
492
+ name: canonicalToolNames.completeGroupPublication,
493
+ description: "Publish only the exact memory ids from a prepared scan after the user explicitly confirms the preview. confirmed must be true. Completing an empty selection safely advances the scan cursor without publishing.",
494
+ inputSchema: {
495
+ type: "object",
496
+ properties: {
497
+ scanId: { type: "string" },
498
+ memoryIds: { type: "array", maxItems: 50, items: { type: "string" } },
499
+ confirmed: { type: "boolean", const: true },
500
+ selectionReason: { type: "string" },
501
+ },
502
+ required: ["scanId", "memoryIds", "confirmed"],
503
+ },
504
+ },
505
+ {
506
+ name: canonicalToolNames.publishToGroup,
507
+ description: "Publish one exact memory snapshot to your current company group without changing the memory's global is_public or encryption state. Call only after the user explicitly asks to publish or push that memory. The operation is idempotent.",
508
+ inputSchema: {
509
+ type: "object",
510
+ properties: {
511
+ memoryId: {
512
+ type: "string",
513
+ description: "Exact id of a memory owned by the current user.",
514
+ },
515
+ triggerMessage: {
516
+ type: "string",
517
+ description: "Optional user message that explicitly requested publication.",
518
+ },
519
+ triggerMessageRole: { type: "string", default: "user" },
520
+ },
521
+ required: ["memoryId"],
522
+ },
523
+ },
524
+ {
525
+ name: canonicalToolNames.publishBatchToGroup,
526
+ description: "Publish up to 50 exact memory snapshots to your current company group without changing global is_public or the encrypted originals. For a session request, first call get_memories_by_context, select the exact ids that match the user's instruction, then pass those ids with the same contextId. Call only after explicit group-publication intent; report what was selected when the user delegates work-related filtering.",
527
+ inputSchema: {
528
+ type: "object",
529
+ properties: {
530
+ memoryIds: {
531
+ type: "array",
532
+ minItems: 1,
533
+ maxItems: 50,
534
+ items: { type: "string" },
535
+ description: "Exact ids owned by the current user.",
536
+ },
537
+ contextId: {
538
+ type: "string",
539
+ description: "Optional session context that every supplied memory must belong to.",
540
+ },
541
+ selectionReason: {
542
+ type: "string",
543
+ description: "Short explanation to return when the agent selected a subset, such as work-related memories.",
544
+ },
545
+ triggerMessage: {
546
+ type: "string",
547
+ description: "Optional user message that explicitly requested publication.",
548
+ },
549
+ triggerMessageRole: { type: "string", default: "user" },
550
+ },
551
+ required: ["memoryIds"],
552
+ },
553
+ },
339
554
  {
340
555
  name: canonicalToolNames.delete,
341
556
  description: "Delete one of the user's EchoMem memories only after explicit user confirmation. First call with memoryId and confirmed omitted/false to preview the target and receive a confirmationToken; this first call never deletes. Only call again with confirmed=true and that exact confirmationToken after the user explicitly confirms deletion. Deletes the memory row only; raw source_of_truth conversation records are preserved.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.22",
3
+ "version": "1.4.23",
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",