@echomem/mcp 1.4.22 → 1.4.24

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, updateGroupProfileSchema, } 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,77 @@ 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 updateGroupProfile(args) {
987
+ const parsed = updateGroupProfileSchema.parse(args ?? {});
988
+ const response = await this.axios.patch("/api/extension/social/groups/current/profile", parsed);
989
+ return response.data;
990
+ }
991
+ async completeGroupPublication(args) {
992
+ const parsed = completeGroupPublicationSchema.parse(args ?? {});
993
+ const enc = await this.encState();
994
+ 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 });
995
+ return response.data;
996
+ }
997
+ async publishMemoryToGroup(args) {
998
+ const parsed = publishToGroupSchema.parse(args ?? {});
999
+ const enc = await this.encState();
1000
+ try {
1001
+ const response = await this.axios.post(`/api/extension/social/groups/current/memories/${encodeURIComponent(parsed.memoryId)}/publish`, {}, {
1002
+ headers: enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : undefined,
1003
+ });
1004
+ return response.data;
1005
+ }
1006
+ catch (error) {
1007
+ throw new Error(`publish_memory_to_group failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
1008
+ }
1009
+ }
1010
+ async publishMemoriesToGroup(args) {
1011
+ const parsed = publishBatchToGroupSchema.parse(args ?? {});
1012
+ const enc = await this.encState();
1013
+ try {
1014
+ const response = await this.axios.post("/api/extension/social/groups/current/memories/publish-batch", {
1015
+ memoryIds: parsed.memoryIds,
1016
+ contextId: parsed.contextId,
1017
+ }, {
1018
+ headers: enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : undefined,
1019
+ });
1020
+ return response.data;
1021
+ }
1022
+ catch (error) {
1023
+ throw new Error(`publish_memories_to_group failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
1024
+ }
1025
+ }
947
1026
  }
948
1027
  const SERVER_VERSION = MCP_PACKAGE_VERSION;
949
1028
  class EchoMemMCPServer {
@@ -1121,6 +1200,24 @@ class EchoMemMCPServer {
1121
1200
  return await this.handleOthers(request.params.arguments);
1122
1201
  case canonicalToolNames.publicMemory:
1123
1202
  return await this.handlePublicMemory(request.params.arguments);
1203
+ case canonicalToolNames.groupContext:
1204
+ return await this.handleGroupContext(request.params.arguments);
1205
+ case canonicalToolNames.createGroup:
1206
+ return await this.handleCreateGroup(request.params.arguments);
1207
+ case canonicalToolNames.createGroupInvite:
1208
+ return await this.handleCreateGroupInvite(request.params.arguments);
1209
+ case canonicalToolNames.joinGroup:
1210
+ return await this.handleJoinGroup(request.params.arguments);
1211
+ case canonicalToolNames.prepareGroupPublication:
1212
+ return await this.handlePrepareGroupPublication(request.params.arguments);
1213
+ case canonicalToolNames.updateGroupProfile:
1214
+ return await this.handleUpdateGroupProfile(request.params.arguments);
1215
+ case canonicalToolNames.completeGroupPublication:
1216
+ return await this.handleCompleteGroupPublication(request.params.arguments);
1217
+ case canonicalToolNames.publishToGroup:
1218
+ return await this.handlePublishToGroup(request.params.arguments);
1219
+ case canonicalToolNames.publishBatchToGroup:
1220
+ return await this.handlePublishBatchToGroup(request.params.arguments);
1124
1221
  case canonicalToolNames.delete:
1125
1222
  return await this.handleDelete(request.params.arguments, rec);
1126
1223
  default:
@@ -1302,7 +1399,7 @@ class EchoMemMCPServer {
1302
1399
  return { content: [{ type: "text", text: "No relevant memories found." }] };
1303
1400
  }
1304
1401
  const formattedResults = memories
1305
- .map((m, idx) => `[Result ${idx + 1}] (Similarity: ${m.similarity_score?.toFixed(3) || "N/A"})
1402
+ .map((m, idx) => `[Result ${idx + 1}] Memory ID: ${m.id || "unknown"} (Similarity: ${m.similarity_score?.toFixed(3) || "N/A"})
1306
1403
  Time: ${m.time} | Location: ${m.location}
1307
1404
  Category: ${m.category} | Object: ${m.object} | Emotion: ${m.emotion}
1308
1405
  Description: ${m.description}
@@ -1504,11 +1601,11 @@ Details: ${m.details || "N/A"}`)
1504
1601
  const memories = payload?.memories ?? [];
1505
1602
  if (!memories.length) {
1506
1603
  const scope = parsed.ownerUserId
1507
- ? ` for friend ${parsed.ownerUserId}`
1604
+ ? ` for peer ${parsed.ownerUserId}`
1508
1605
  : parsed.ownerName
1509
- ? ` for friend ${parsed.ownerName}`
1606
+ ? ` for peer ${parsed.ownerName}`
1510
1607
  : parsed.target
1511
- ? ` for friend ${parsed.target}`
1608
+ ? ` for peer ${parsed.target}`
1512
1609
  : parsed.targetFriendIds?.length
1513
1610
  ? ` for ${parsed.targetFriendIds.length} selected friends`
1514
1611
  : parsed.targetFriendNames?.length
@@ -1521,7 +1618,12 @@ Details: ${m.details || "N/A"}`)
1521
1618
  }
1522
1619
  const metadata = [
1523
1620
  typeof payload?.friendCount === "number" ? `friendCount=${payload.friendCount}` : "",
1524
- typeof payload?.searchedFriendCount === "number" ? `searchedFriendCount=${payload.searchedFriendCount}` : "",
1621
+ typeof payload?.groupPeerCount === "number" ? `groupPeerCount=${payload.groupPeerCount}` : "",
1622
+ typeof payload?.searchedPeerCount === "number"
1623
+ ? `searchedPeerCount=${payload.searchedPeerCount}`
1624
+ : typeof payload?.searchedFriendCount === "number"
1625
+ ? `searchedPeerCount=${payload.searchedFriendCount}`
1626
+ : "",
1525
1627
  typeof payload?.memoryViewsInserted === "number" ? `memoryViewsInserted=${payload.memoryViewsInserted}` : "",
1526
1628
  ].filter(Boolean).join(", ");
1527
1629
  const formattedResults = memories
@@ -1674,6 +1776,197 @@ Details: ${m.details || "N/A"}`;
1674
1776
  content: [{ type: "text", text }],
1675
1777
  };
1676
1778
  }
1779
+ async handleGroupContext(args) {
1780
+ groupContextSchema.parse(args ?? {});
1781
+ const payload = await this.client.getGroupContext(args);
1782
+ const group = isRecord(payload?.group) ? payload.group : null;
1783
+ const participants = Array.isArray(payload?.participants)
1784
+ ? payload.participants.filter(isRecord)
1785
+ : [];
1786
+ const currentParticipant = isRecord(payload?.currentParticipant)
1787
+ ? payload.currentParticipant
1788
+ : null;
1789
+ const coverage = isRecord(payload?.coverage) ? payload.coverage : {};
1790
+ if (!group) {
1791
+ return {
1792
+ content: [{
1793
+ type: "text",
1794
+ text: "You are not assigned to a company group yet. Ask an EchoMem administrator to add you before using group orientation or group publishing.",
1795
+ }],
1796
+ };
1797
+ }
1798
+ const participantText = participants.map((participant, index) => [
1799
+ `[${index + 1}] ${readString(participant, "displayName") ?? "Unnamed participant"}`,
1800
+ `User ID: ${readString(participant, "userId") ?? "unknown"}`,
1801
+ `Title: ${readString(participant, "title") ?? "Not declared"}`,
1802
+ `Declared responsibility: ${readString(participant, "responsibilitySummary") ?? "Not provided"}`,
1803
+ `Published memories: ${readNumber(participant, "publicMemoryCount") ?? 0}`,
1804
+ ].join("\n")).join("\n\n");
1805
+ const participantCount = readNumber(coverage, "participantCount") ?? participants.length;
1806
+ const coveredCount = readNumber(coverage, "participantsWithPublishedMemories") ?? 0;
1807
+ const totalPublished = readNumber(coverage, "totalPublishedMemories") ?? 0;
1808
+ const text = [
1809
+ `Company group: ${readString(group, "name") ?? "Unnamed group"}`,
1810
+ readString(group, "description") ? `Description: ${readString(group, "description")}` : "",
1811
+ `Coverage: ${coveredCount} of ${participantCount} participants have published memories (${totalPublished} total).`,
1812
+ "",
1813
+ participantText,
1814
+ "",
1815
+ "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.",
1816
+ currentParticipant
1817
+ && (!readString(currentParticipant, "title") || !readString(currentParticipant, "responsibilitySummary"))
1818
+ ? "Your group profile is incomplete. Use prepare_group_publication to review your memory evidence, propose the missing fields, and save them only after confirmation with update_group_profile."
1819
+ : "",
1820
+ ].filter(Boolean).join("\n");
1821
+ return { content: [{ type: "text", text }] };
1822
+ }
1823
+ async handlePublishToGroup(args) {
1824
+ const parsed = publishToGroupSchema.parse(args ?? {});
1825
+ const payload = await this.client.publishMemoryToGroup(args);
1826
+ if (payload?.success === false) {
1827
+ throw new Error(`EchoMem API Error: ${payload.error || "publish failed"}`);
1828
+ }
1829
+ const group = isRecord(payload?.group) ? payload.group : {};
1830
+ const groupName = readString(group, "name") ?? "your company group";
1831
+ const memberCount = typeof payload?.searchableByGroupMemberCount === "number"
1832
+ ? payload.searchableByGroupMemberCount
1833
+ : 0;
1834
+ const alreadyPublished = payload?.alreadyPublished === true;
1835
+ return {
1836
+ content: [{
1837
+ type: "text",
1838
+ text: alreadyPublished
1839
+ ? `Memory ${parsed.memoryId} was already published to ${groupName}. It is searchable by up to ${memberCount} other group members.`
1840
+ : `Published memory ${parsed.memoryId} to ${groupName}. It is now searchable by up to ${memberCount} other group members.`,
1841
+ }],
1842
+ };
1843
+ }
1844
+ async handleCreateGroup(args) {
1845
+ const parsed = createGroupSchema.parse(args ?? {});
1846
+ const payload = await this.client.createGroup(parsed);
1847
+ return { content: [{ type: "text", text: `Created company group ${payload?.group?.name ?? parsed.name}. Generate an invite code when you are ready to add teammates.` }] };
1848
+ }
1849
+ async handleCreateGroupInvite(args) {
1850
+ createGroupInviteSchema.parse(args ?? {});
1851
+ const payload = await this.client.createGroupInvite(args);
1852
+ return {
1853
+ content: [{
1854
+ type: "text",
1855
+ text: [
1856
+ `Invite code for ${payload?.group?.name ?? "your group"}: ${payload?.code}`,
1857
+ `Expires: ${payload?.expiresAt}`,
1858
+ `Maximum uses: ${payload?.maxUses}`,
1859
+ "Share this secret code only with intended teammates.",
1860
+ ].join("\n"),
1861
+ }],
1862
+ };
1863
+ }
1864
+ async handleJoinGroup(args) {
1865
+ const parsed = joinGroupSchema.parse(args ?? {});
1866
+ const payload = await this.client.joinGroup(parsed);
1867
+ return {
1868
+ content: [{
1869
+ type: "text",
1870
+ text: payload?.alreadyMember
1871
+ ? `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.`
1872
+ : `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.`,
1873
+ }],
1874
+ };
1875
+ }
1876
+ async handlePrepareGroupPublication(args) {
1877
+ const parsed = prepareGroupPublicationSchema.parse(args ?? {});
1878
+ const payload = await this.client.prepareGroupPublication(parsed);
1879
+ const candidates = Array.isArray(payload?.candidates) ? payload.candidates : [];
1880
+ const participantProfile = isRecord(payload?.participantProfile)
1881
+ ? payload.participantProfile
1882
+ : {};
1883
+ const formatted = candidates.map((candidate, index) => [
1884
+ `[${index + 1}] Memory ID: ${readString(candidate, "memoryId") ?? "unknown"}`,
1885
+ `Created: ${readString(candidate, "createdAt") ?? "unknown"}`,
1886
+ `Category: ${readString(candidate, "category") ?? "unknown"}`,
1887
+ `Description: ${readString(candidate, "description") ?? ""}`,
1888
+ readString(candidate, "details") ? `Details: ${readString(candidate, "details")}` : "",
1889
+ `Already published: ${candidate.alreadyPublished === true ? "yes" : "no"}`,
1890
+ `Exact content duplicate: ${candidate.exactContentDuplicate === true ? "yes" : "no"}`,
1891
+ ].filter(Boolean).join("\n")).join("\n\n");
1892
+ return {
1893
+ content: [{
1894
+ type: "text",
1895
+ text: [
1896
+ `Prepared publication scan ${payload?.scanId} for ${payload?.group?.name ?? "your group"}.`,
1897
+ `Window: ${payload?.windowStart ?? "context start"} → ${payload?.windowEnd}`,
1898
+ `Candidates: ${candidates.length}. Nothing has been published.`,
1899
+ `Current title: ${readString(participantProfile, "title") ?? "Not declared"}`,
1900
+ `Current declared responsibility: ${readString(participantProfile, "responsibilitySummary") ?? "Not provided"}`,
1901
+ "",
1902
+ formatted,
1903
+ "",
1904
+ "Select exact memory IDs matching the user's instruction. From memory evidence, draft a concise title and responsibility summary. Show the profile proposal and memory preview together, then ask for one explicit confirmation. After confirmation, call update_group_profile and complete_group_publication.",
1905
+ ].join("\n"),
1906
+ }],
1907
+ };
1908
+ }
1909
+ async handleUpdateGroupProfile(args) {
1910
+ const parsed = updateGroupProfileSchema.parse(args ?? {});
1911
+ const payload = await this.client.updateGroupProfile(parsed);
1912
+ const participant = isRecord(payload?.participant) ? payload.participant : {};
1913
+ return {
1914
+ content: [{
1915
+ type: "text",
1916
+ text: [
1917
+ "Confirmed company-group profile updated.",
1918
+ `Display name: ${readString(participant, "displayName") ?? parsed.displayName ?? "unchanged"}`,
1919
+ `Title: ${readString(participant, "title") ?? parsed.title}`,
1920
+ `Declared responsibility: ${readString(participant, "responsibilitySummary") ?? parsed.responsibilitySummary}`,
1921
+ "These fields are now declared directory facts visible to group members.",
1922
+ ].join("\n"),
1923
+ }],
1924
+ };
1925
+ }
1926
+ async handleCompleteGroupPublication(args) {
1927
+ const parsed = completeGroupPublicationSchema.parse(args ?? {});
1928
+ const payload = await this.client.completeGroupPublication(parsed);
1929
+ const memories = Array.isArray(payload?.memories) ? payload.memories : [];
1930
+ const published = memories.filter((memory) => memory.alreadyPublished !== true && memory.skippedDuplicate !== true).length;
1931
+ const duplicates = memories.filter((memory) => memory.skippedDuplicate === true).length;
1932
+ return {
1933
+ content: [{
1934
+ type: "text",
1935
+ text: [
1936
+ `Completed publication scan ${parsed.scanId}.`,
1937
+ `Newly published: ${published}.`,
1938
+ `Exact duplicates skipped: ${duplicates}.`,
1939
+ `Selected memory IDs: ${parsed.memoryIds.join(", ") || "(none)"}.`,
1940
+ "The scan cursor advanced; encrypted originals and global public settings were unchanged.",
1941
+ ].join("\n"),
1942
+ }],
1943
+ };
1944
+ }
1945
+ async handlePublishBatchToGroup(args) {
1946
+ const parsed = publishBatchToGroupSchema.parse(args ?? {});
1947
+ const payload = await this.client.publishMemoriesToGroup(parsed);
1948
+ if (payload?.success === false) {
1949
+ throw new Error(`EchoMem API Error: ${payload.error || "batch publish failed"}`);
1950
+ }
1951
+ const group = isRecord(payload?.group) ? payload.group : {};
1952
+ const groupName = readString(group, "name") ?? "your company group";
1953
+ const publishedCount = typeof payload?.publishedCount === "number" ? payload.publishedCount : 0;
1954
+ const alreadyPublishedCount = typeof payload?.alreadyPublishedCount === "number"
1955
+ ? payload.alreadyPublishedCount
1956
+ : 0;
1957
+ return {
1958
+ content: [{
1959
+ type: "text",
1960
+ text: [
1961
+ `Published ${publishedCount} memory snapshots to ${groupName}.`,
1962
+ alreadyPublishedCount ? `${alreadyPublishedCount} were already published and were refreshed.` : "",
1963
+ `Selected memory IDs: ${parsed.memoryIds.join(", ")}`,
1964
+ parsed.selectionReason ? `Selection reason: ${parsed.selectionReason}` : "",
1965
+ "The encrypted originals and their global public settings were not changed.",
1966
+ ].filter(Boolean).join("\n"),
1967
+ }],
1968
+ };
1969
+ }
1677
1970
  async handleDelete(args, rec) {
1678
1971
  const parsed = deleteMemorySchema.parse(args ?? {});
1679
1972
  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,9 @@ 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-publication preview.",
32
+ "After joining or when profile fields are missing, use candidate memory evidence to propose a title and responsibility summary. Ask the user to confirm that proposal together with the publication preview, then call update_group_profile and complete_group_publication.",
33
+ "Never save an inferred group profile or complete a group publication without explicit confirmation, and never store or log an echo_grp_ invite code.",
31
34
  ].join(" ");
32
35
  export function withMcpVersion(description) {
33
36
  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,16 @@ 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. After joining, continue into the profile-and-publication preview instead of leaving title or responsibility blank.",
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-publication 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 and exclude already-published or exact-content duplicates. Use the candidate evidence to draft a concise title and responsibility summary for the current member, but label both as proposals rather than facts.",
402
+ "- Present the proposed title/responsibility and the memory publication preview together and ask for one explicit confirmation. Never save an inferred profile or publish memories before confirmation.",
403
+ "- On confirmation, call `update_group_profile` with the confirmed title, responsibility summary, and `confirmed: true`, then call `complete_group_publication` with the exact `scanId`, selected memory IDs, and `confirmed: true`. If the user edits either proposal, use their wording. An explicit request to join and upload still requires this combined preview and confirmation.",
394
404
  "- 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
405
  "- If the user wants the HUD to come back after a computer restart, run the shell command `echomem-hud autostart on --client auto`.",
396
406
  AGENTS_MD_END,
@@ -1716,6 +1726,7 @@ export function startCallbackServer(opts = {}) {
1716
1726
  return;
1717
1727
  }
1718
1728
  if (route === "/billing-status" && req.method === "GET") {
1729
+ res.setHeader("Cache-Control", "no-store, max-age=0");
1719
1730
  if (!checkNonce(url.searchParams.get("nonce") || undefined))
1720
1731
  return void text(res, 403, "bad nonce");
1721
1732
  if (requiresReportConsent && !reportConsentGranted) {
@@ -10,6 +10,15 @@ 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
+ updateGroupProfile: "update_group_profile",
19
+ completeGroupPublication: "complete_group_publication",
20
+ publishToGroup: "publish_memory_to_group",
21
+ publishBatchToGroup: "publish_memories_to_group",
13
22
  report: "echomem_usage_report",
14
23
  updateStatus: "echomem_update_status",
15
24
  contextHealth: "echo_context_health",
@@ -59,9 +68,17 @@ export const timeRangeSchema = z.object({
59
68
  endDate: z.string(),
60
69
  limit: z.number().optional().default(50),
61
70
  });
71
+ const keywordListSchema = z.preprocess((value) => {
72
+ if (typeof value !== "string")
73
+ return value;
74
+ return value
75
+ .split(",")
76
+ .map((keyword) => keyword.trim())
77
+ .filter(Boolean);
78
+ }, z.array(z.string().trim().min(1)).min(1).max(50));
62
79
  export const keywordsSchema = z.object({
63
80
  ...triggerMetadataSchema,
64
- keywords: z.array(z.string()),
81
+ keywords: keywordListSchema,
65
82
  limit: z.number().optional().default(10),
66
83
  });
67
84
  export const listFriendsSchema = z.object({
@@ -94,6 +111,63 @@ export const publicMemorySchema = z.object({
94
111
  ...triggerMetadataSchema,
95
112
  memoryId: z.string().min(1),
96
113
  });
114
+ export const groupContextSchema = z.object({
115
+ ...triggerMetadataSchema,
116
+ });
117
+ export const createGroupSchema = z.object({
118
+ ...triggerMetadataSchema,
119
+ name: z.string().min(1).max(120),
120
+ description: z.string().max(1000).optional(),
121
+ displayName: z.string().min(1).max(120),
122
+ title: z.string().max(160).optional(),
123
+ responsibilitySummary: z.string().max(1000).optional(),
124
+ });
125
+ export const createGroupInviteSchema = z.object({
126
+ ...triggerMetadataSchema,
127
+ expiresInDays: z.number().int().min(1).max(30).optional(),
128
+ maxUses: z.number().int().min(1).max(100).optional(),
129
+ });
130
+ export const joinGroupSchema = z.object({
131
+ ...triggerMetadataSchema,
132
+ code: z.string().min(30),
133
+ displayName: z.string().min(1).max(120),
134
+ title: z.string().max(160).optional(),
135
+ responsibilitySummary: z.string().max(1000).optional(),
136
+ });
137
+ export const prepareGroupPublicationSchema = z.object({
138
+ ...triggerMetadataSchema,
139
+ scope: z.enum(["bootstrap", "since_last_scan", "context", "time_range"]).default("since_last_scan"),
140
+ contextId: z.string().min(1).optional(),
141
+ startAt: z.string().optional(),
142
+ endAt: z.string().optional(),
143
+ lookbackDays: z.number().int().min(1).max(365).optional(),
144
+ limit: z.number().int().min(1).max(50).optional(),
145
+ instruction: z.string().max(500).optional(),
146
+ });
147
+ export const updateGroupProfileSchema = z.object({
148
+ ...triggerMetadataSchema,
149
+ displayName: z.string().min(1).max(120).optional(),
150
+ title: z.string().min(1).max(160),
151
+ responsibilitySummary: z.string().min(1).max(1000),
152
+ confirmed: z.literal(true),
153
+ });
154
+ export const completeGroupPublicationSchema = z.object({
155
+ ...triggerMetadataSchema,
156
+ scanId: z.string().min(1),
157
+ memoryIds: z.array(z.string().min(1)).max(50),
158
+ confirmed: z.literal(true),
159
+ selectionReason: z.string().max(500).optional(),
160
+ });
161
+ export const publishToGroupSchema = z.object({
162
+ ...triggerMetadataSchema,
163
+ memoryId: z.string().min(1),
164
+ });
165
+ export const publishBatchToGroupSchema = z.object({
166
+ ...triggerMetadataSchema,
167
+ memoryIds: z.array(z.string().min(1)).min(1).max(50),
168
+ contextId: z.string().min(1).optional(),
169
+ selectionReason: z.string().max(500).optional(),
170
+ });
97
171
  export const deleteMemorySchema = z.object({
98
172
  memoryId: z.string().min(1),
99
173
  confirmed: z.boolean().optional().default(false),
@@ -108,7 +182,7 @@ export function listToolSpecs(opts = {}) {
108
182
  const currentTime = new Date().toISOString();
109
183
  const map = opts.map?.trim();
110
184
  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.";
185
+ const recallPlanNote = "Available on every plan: Free includes 100 searches each week, Pro includes 500, and Power includes 2,000.";
112
186
  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
187
  const updateSection = updateNotice ? `\n\nUPDATE NOTICE: ${updateNotice}` : "";
114
188
  const mapSection = map
@@ -204,11 +278,27 @@ export function listToolSpecs(opts = {}) {
204
278
  },
205
279
  {
206
280
  name: canonicalToolNames.keywords,
207
- description: `Search memories based on keywords in keys field. ${recallPlanNote}`,
281
+ 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
282
  inputSchema: {
209
283
  type: "object",
210
284
  properties: {
211
- keywords: { type: "array", items: { type: "string" } },
285
+ keywords: {
286
+ oneOf: [
287
+ {
288
+ type: "array",
289
+ minItems: 1,
290
+ maxItems: 50,
291
+ items: { type: "string", minLength: 1 },
292
+ },
293
+ {
294
+ type: "string",
295
+ minLength: 1,
296
+ description: "Compatibility form: one comma-separated JSON string, such as \"flow-lab, flow.html, Rive\".",
297
+ },
298
+ ],
299
+ description: "Use a JSON array of quoted strings. Do not pass unquoted comma-separated tokens.",
300
+ examples: [["flow-lab", "flow.html", "Rive"]],
301
+ },
212
302
  limit: { type: "number", default: 10 },
213
303
  triggerMessage: {
214
304
  type: "string",
@@ -274,7 +364,7 @@ export function listToolSpecs(opts = {}) {
274
364
  },
275
365
  {
276
366
  name: canonicalToolNames.others,
277
- description: "Search accepted friends' public memories. Returned memories are recorded in the existing memory_views pipeline for the memory owners.",
367
+ 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
368
  inputSchema: {
279
369
  type: "object",
280
370
  properties: {
@@ -282,25 +372,25 @@ export function listToolSpecs(opts = {}) {
282
372
  limit: { type: "number", default: 10 },
283
373
  target: {
284
374
  type: "string",
285
- description: "Accepted-friend user id or exact display name/username. Prefer this for @Name asks.",
375
+ description: "Accessible friend or group-member user id or exact display name. Prefer this for @Name asks.",
286
376
  },
287
377
  ownerUserId: {
288
378
  type: "string",
289
- description: "Optional accepted-friend user id to scope the search to one friend.",
379
+ description: "Optional accessible user id to scope the search to one person.",
290
380
  },
291
381
  ownerName: {
292
382
  type: "string",
293
- description: "Optional accepted-friend display name/username to scope the search to one friend.",
383
+ description: "Optional accessible friend or group-member display name to scope the search.",
294
384
  },
295
385
  targetFriendIds: {
296
386
  type: "array",
297
387
  items: { type: "string" },
298
- description: "Optional accepted-friend user ids to scope the search.",
388
+ description: "Legacy field for optional accessible friend or group-member user ids.",
299
389
  },
300
390
  targetFriendNames: {
301
391
  type: "array",
302
392
  items: { type: "string" },
303
- description: "Optional accepted-friend display names/usernames to scope the search.",
393
+ description: "Legacy field for optional accessible friend or group-member display names.",
304
394
  },
305
395
  recordAccess: {
306
396
  type: "boolean",
@@ -319,7 +409,7 @@ export function listToolSpecs(opts = {}) {
319
409
  },
320
410
  {
321
411
  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.",
412
+ 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
413
  inputSchema: {
324
414
  type: "object",
325
415
  properties: {
@@ -336,6 +426,153 @@ export function listToolSpecs(opts = {}) {
336
426
  required: ["memoryId"],
337
427
  },
338
428
  },
429
+ {
430
+ name: canonicalToolNames.groupContext,
431
+ 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.",
432
+ inputSchema: {
433
+ type: "object",
434
+ properties: {
435
+ triggerMessage: {
436
+ type: "string",
437
+ description: "Optional user message that caused this group-orientation lookup.",
438
+ },
439
+ triggerMessageRole: { type: "string", default: "user" },
440
+ },
441
+ },
442
+ },
443
+ {
444
+ name: canonicalToolNames.createGroup,
445
+ 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.",
446
+ inputSchema: {
447
+ type: "object",
448
+ properties: {
449
+ name: { type: "string" },
450
+ description: { type: "string" },
451
+ displayName: { type: "string" },
452
+ title: { type: "string" },
453
+ responsibilitySummary: { type: "string" },
454
+ },
455
+ required: ["name", "displayName"],
456
+ },
457
+ },
458
+ {
459
+ name: canonicalToolNames.createGroupInvite,
460
+ 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.",
461
+ inputSchema: {
462
+ type: "object",
463
+ properties: {
464
+ expiresInDays: { type: "number", default: 7 },
465
+ maxUses: { type: "number", default: 20 },
466
+ },
467
+ },
468
+ },
469
+ {
470
+ name: canonicalToolNames.joinGroup,
471
+ 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.",
472
+ inputSchema: {
473
+ type: "object",
474
+ properties: {
475
+ code: { type: "string" },
476
+ displayName: { type: "string" },
477
+ title: { type: "string" },
478
+ responsibilitySummary: { type: "string" },
479
+ },
480
+ required: ["code", "displayName"],
481
+ },
482
+ },
483
+ {
484
+ name: canonicalToolNames.prepareGroupPublication,
485
+ description: "Prepare a no-publication 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, drafts a concise title and responsibility summary from memory evidence, and asks the user to confirm both the profile and publication selection together. This tool never publishes.",
486
+ inputSchema: {
487
+ type: "object",
488
+ properties: {
489
+ scope: { type: "string", enum: ["bootstrap", "since_last_scan", "context", "time_range"], default: "since_last_scan" },
490
+ contextId: { type: "string" },
491
+ startAt: { type: "string" },
492
+ endAt: { type: "string" },
493
+ lookbackDays: { type: "number", default: 30 },
494
+ limit: { type: "number", default: 50 },
495
+ instruction: { type: "string", default: "work-related" },
496
+ },
497
+ },
498
+ },
499
+ {
500
+ name: canonicalToolNames.updateGroupProfile,
501
+ description: "Save the current member's declared group title and responsibility summary only after the user explicitly confirms the agent's proposal. These become directory facts visible to group members; never save an unconfirmed inference.",
502
+ inputSchema: {
503
+ type: "object",
504
+ properties: {
505
+ displayName: { type: "string" },
506
+ title: { type: "string" },
507
+ responsibilitySummary: { type: "string" },
508
+ confirmed: { type: "boolean", const: true },
509
+ },
510
+ required: ["title", "responsibilitySummary", "confirmed"],
511
+ },
512
+ },
513
+ {
514
+ name: canonicalToolNames.completeGroupPublication,
515
+ 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.",
516
+ inputSchema: {
517
+ type: "object",
518
+ properties: {
519
+ scanId: { type: "string" },
520
+ memoryIds: { type: "array", maxItems: 50, items: { type: "string" } },
521
+ confirmed: { type: "boolean", const: true },
522
+ selectionReason: { type: "string" },
523
+ },
524
+ required: ["scanId", "memoryIds", "confirmed"],
525
+ },
526
+ },
527
+ {
528
+ name: canonicalToolNames.publishToGroup,
529
+ 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.",
530
+ inputSchema: {
531
+ type: "object",
532
+ properties: {
533
+ memoryId: {
534
+ type: "string",
535
+ description: "Exact id of a memory owned by the current user.",
536
+ },
537
+ triggerMessage: {
538
+ type: "string",
539
+ description: "Optional user message that explicitly requested publication.",
540
+ },
541
+ triggerMessageRole: { type: "string", default: "user" },
542
+ },
543
+ required: ["memoryId"],
544
+ },
545
+ },
546
+ {
547
+ name: canonicalToolNames.publishBatchToGroup,
548
+ 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.",
549
+ inputSchema: {
550
+ type: "object",
551
+ properties: {
552
+ memoryIds: {
553
+ type: "array",
554
+ minItems: 1,
555
+ maxItems: 50,
556
+ items: { type: "string" },
557
+ description: "Exact ids owned by the current user.",
558
+ },
559
+ contextId: {
560
+ type: "string",
561
+ description: "Optional session context that every supplied memory must belong to.",
562
+ },
563
+ selectionReason: {
564
+ type: "string",
565
+ description: "Short explanation to return when the agent selected a subset, such as work-related memories.",
566
+ },
567
+ triggerMessage: {
568
+ type: "string",
569
+ description: "Optional user message that explicitly requested publication.",
570
+ },
571
+ triggerMessageRole: { type: "string", default: "user" },
572
+ },
573
+ required: ["memoryIds"],
574
+ },
575
+ },
339
576
  {
340
577
  name: canonicalToolNames.delete,
341
578
  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.24",
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",