@echomem/mcp 1.4.41 → 1.4.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1096,6 +1096,8 @@ class EchoMemApiClient {
1096
1096
  kPerUser: parsed.kPerUser,
1097
1097
  similarityThreshold: parsed.similarityThreshold,
1098
1098
  timeFrameDays: parsed.timeFrameDays,
1099
+ workspaceId: parsed.workspaceId ?? parsed.groupId,
1100
+ scope: parsed.scope,
1099
1101
  requestId: this.sessionId,
1100
1102
  source: "mcp_friend_public_memory_search",
1101
1103
  });
@@ -1108,7 +1110,14 @@ class EchoMemApiClient {
1108
1110
  async getPublicMemory(args) {
1109
1111
  const parsed = publicMemorySchema.parse(args ?? {});
1110
1112
  try {
1111
- const response = await this.axios.get(`/api/extension/social/public-memories/${encodeURIComponent(parsed.memoryId)}?requestId=${encodeURIComponent(this.sessionId)}&source=mcp_friend_public_memory_fetch`);
1113
+ const workspaceId = parsed.workspaceId ?? parsed.groupId;
1114
+ const query = new URLSearchParams({
1115
+ requestId: this.sessionId,
1116
+ source: "mcp_friend_public_memory_fetch",
1117
+ });
1118
+ if (workspaceId)
1119
+ query.set("workspaceId", workspaceId);
1120
+ const response = await this.axios.get(`/api/extension/social/public-memories/${encodeURIComponent(parsed.memoryId)}?${query.toString()}`);
1112
1121
  return response.data;
1113
1122
  }
1114
1123
  catch (error) {
@@ -1131,9 +1140,13 @@ class EchoMemApiClient {
1131
1140
  }
1132
1141
  }
1133
1142
  async getGroupContext(args) {
1134
- groupContextSchema.parse(args ?? {});
1143
+ const parsed = groupContextSchema.parse(args ?? {});
1135
1144
  try {
1136
- const response = await this.axios.get("/api/extension/social/groups/current");
1145
+ const workspaceId = parsed.workspaceId ?? parsed.groupId;
1146
+ const path = workspaceId
1147
+ ? `/api/extension/social/groups/current?workspaceId=${encodeURIComponent(workspaceId)}`
1148
+ : "/api/extension/social/groups/current";
1149
+ const response = await this.axios.get(path);
1137
1150
  return response.data;
1138
1151
  }
1139
1152
  catch (error) {
@@ -1197,6 +1210,7 @@ class EchoMemApiClient {
1197
1210
  const enc = await this.encState();
1198
1211
  try {
1199
1212
  const response = await this.axios.post(`/api/extension/social/groups/current/memories/${encodeURIComponent(parsed.memoryId)}/publish`, {
1213
+ workspaceId: parsed.workspaceId ?? parsed.groupId,
1200
1214
  acknowledgedFlaggedMemoryIds: parsed.acknowledgedFlaggedMemoryIds,
1201
1215
  }, {
1202
1216
  headers: enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : undefined,
@@ -1212,6 +1226,7 @@ class EchoMemApiClient {
1212
1226
  const enc = await this.encState();
1213
1227
  try {
1214
1228
  const response = await this.axios.post("/api/extension/social/groups/current/memories/publish-batch", {
1229
+ workspaceId: parsed.workspaceId ?? parsed.groupId,
1215
1230
  memoryIds: parsed.memoryIds,
1216
1231
  contextId: parsed.contextId,
1217
1232
  selectionReason: parsed.selectionReason,
@@ -1995,6 +2010,28 @@ Details: ${m.details || "N/A"}`)
1995
2010
  async handleOthers(args) {
1996
2011
  const parsed = othersSchema.parse(args ?? {});
1997
2012
  const payload = await this.client.searchOthersMemories(args);
2013
+ if (payload?.requiresWorkspaceSelection === true) {
2014
+ const availableWorkspaces = Array.isArray(payload?.availableWorkspaces)
2015
+ ? payload.availableWorkspaces.filter(isRecord)
2016
+ : [];
2017
+ const choices = availableWorkspaces
2018
+ .map((workspace) => `${readString(workspace, "name") ?? "Unnamed workspace"} (${readString(workspace, "id") ?? "unknown id"})`)
2019
+ .join(", ");
2020
+ return {
2021
+ content: [{
2022
+ type: "text",
2023
+ text: `You belong to ${availableWorkspaces.length} workspaces: ${choices}. Ask the user which workspace's teammates to search, then call search_others_memories again with workspaceId set to one of those ids. (To search a specific friend instead, pass that person as target.)`,
2024
+ }],
2025
+ };
2026
+ }
2027
+ if (payload?.groupScopeUnavailable === true) {
2028
+ return {
2029
+ content: [{
2030
+ type: "text",
2031
+ text: "You are not in a company workspace, so there are no teammates to search. To search friends instead, call search_others_memories again with scope \"friends\", or pass a specific person as target.",
2032
+ }],
2033
+ };
2034
+ }
1998
2035
  const memories = payload?.memories ?? [];
1999
2036
  const authenticatedViewer = isRecord(payload?.authenticatedViewer)
2000
2037
  ? payload.authenticatedViewer
@@ -2216,6 +2253,20 @@ Details: ${m.details || "N/A"}`;
2216
2253
  async handleGroupContext(args) {
2217
2254
  groupContextSchema.parse(args ?? {});
2218
2255
  const payload = await this.client.getGroupContext(args);
2256
+ if (payload?.requiresWorkspaceSelection === true) {
2257
+ const availableWorkspaces = Array.isArray(payload?.availableWorkspaces)
2258
+ ? payload.availableWorkspaces.filter(isRecord)
2259
+ : [];
2260
+ const choices = availableWorkspaces
2261
+ .map((workspace) => `${readString(workspace, "name") ?? "Unnamed workspace"} (${readString(workspace, "id") ?? "unknown id"})`)
2262
+ .join(", ");
2263
+ return {
2264
+ content: [{
2265
+ type: "text",
2266
+ text: `You belong to ${availableWorkspaces.length} workspaces: ${choices}. Ask the user which workspace to show context for, then call get_group_context again with workspaceId set to one of those ids.`,
2267
+ }],
2268
+ };
2269
+ }
2219
2270
  const group = isRecord(payload?.group) ? payload.group : null;
2220
2271
  const participants = Array.isArray(payload?.participants)
2221
2272
  ? payload.participants.filter(isRecord)
package/dist/setup.js CHANGED
@@ -416,13 +416,70 @@ export function writeJsonClientConfig(configPath, entry) {
416
416
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
417
417
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
418
418
  }
419
- export function writeClaudeCodeConfig(entry) {
420
- // EchoMem is a memory server that should load in EVERY Claude Code project, so it belongs at
421
- // `user` scope (~/.claude.json, all projects) rather than `local` scope (the current project only).
422
- const addArguments = ["mcp", "add-json", "-s", "user", "echomem", JSON.stringify(entry)];
423
- const removeFromScope = (scope) => {
419
+ function readClaudeCodeConfigFile(configPath) {
420
+ try {
421
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
422
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
423
+ ? parsed
424
+ : {};
425
+ }
426
+ catch {
427
+ return {};
428
+ }
429
+ }
430
+ function echoMemEntryFromServers(value) {
431
+ if (!value || typeof value !== "object" || Array.isArray(value))
432
+ return undefined;
433
+ const entry = value.echomem;
434
+ return entry && typeof entry === "object" && !Array.isArray(entry)
435
+ ? entry
436
+ : undefined;
437
+ }
438
+ function claudeEntriesMatch(actual, expected) {
439
+ if (!actual || actual.command !== expected.command)
440
+ return false;
441
+ const actualArgs = Array.isArray(actual.args) ? actual.args : [];
442
+ const expectedArgs = Array.isArray(expected.args) ? expected.args : [];
443
+ if (actualArgs.length !== expectedArgs.length || actualArgs.some((value, index) => value !== expectedArgs[index])) {
444
+ return false;
445
+ }
446
+ const expectedEnv = expected.env;
447
+ if (!expectedEnv || typeof expectedEnv !== "object" || Array.isArray(expectedEnv))
448
+ return true;
449
+ const actualEnv = actual.env;
450
+ if (!actualEnv || typeof actualEnv !== "object" || Array.isArray(actualEnv))
451
+ return false;
452
+ return Object.entries(expectedEnv).every(([key, value]) => actualEnv[key] === value);
453
+ }
454
+ function claudeCodeLocalEchoMemProjects(configPath) {
455
+ const projects = readClaudeCodeConfigFile(configPath).projects;
456
+ if (!projects || typeof projects !== "object" || Array.isArray(projects))
457
+ return [];
458
+ return Object.entries(projects)
459
+ .filter(([, value]) => {
460
+ if (!value || typeof value !== "object" || Array.isArray(value))
461
+ return false;
462
+ return Boolean(echoMemEntryFromServers(value.mcpServers));
463
+ })
464
+ .map(([projectPath]) => projectPath)
465
+ .sort();
466
+ }
467
+ export function writeClaudeCodeConfig(entry, options = {}) {
468
+ // EchoMem belongs at user scope so every Claude Code project resolves the same durable runtime.
469
+ // Older CLI versions wrote local/project entries, which take precedence over user scope and can
470
+ // keep launching a deleted npm cache or stale runtime. Migrate those only after user scope is safe.
471
+ const configPath = options.configPath ?? home(".claude.json");
472
+ const emptyResult = () => ({
473
+ state: "unavailable",
474
+ removedLocalProjects: [],
475
+ skippedLocalProjects: [],
476
+ failedLocalProjects: [],
477
+ restoredPreviousUserEntry: false,
478
+ });
479
+ const runClaude = (args, cwd) => {
424
480
  try {
425
- execFileSync("claude", ["mcp", "remove", "echomem", "-s", scope], {
481
+ execFileSync("claude", args, {
482
+ cwd,
426
483
  encoding: "utf8",
427
484
  stdio: ["ignore", "pipe", "pipe"],
428
485
  timeout: 10000,
@@ -433,39 +490,61 @@ export function writeClaudeCodeConfig(entry) {
433
490
  return false;
434
491
  }
435
492
  };
436
- // Older builds installed EchoMem at `local` scope. Left in place it would shadow the user-scoped
437
- // entry and keep launching the stale command, so drop it first. Best-effort: `local` is per-project,
438
- // so this only clears the directory setup runs from — a no-op (ignored) when nothing is there.
439
- removeFromScope("local");
440
- try {
441
- execFileSync("claude", addArguments, {
442
- encoding: "utf8",
443
- stdio: ["ignore", "pipe", "pipe"],
444
- timeout: 10000,
445
- });
446
- return "wrote";
447
- }
448
- catch (error) {
449
- const stderr = error.stderr;
450
- const detail = Buffer.isBuffer(stderr) ? stderr.toString("utf8") : String(stderr ?? "");
451
- if (!detail.includes("already exists"))
452
- return "unavailable";
453
- }
454
- // Claude Code's CLI will not replace a same-name server. Once the replacement entry is fully
455
- // constructed, remove only EchoMem and immediately re-add it; sibling MCP servers remain.
456
- try {
457
- if (!removeFromScope("user"))
458
- return "unavailable";
459
- execFileSync("claude", addArguments, {
460
- encoding: "utf8",
461
- stdio: ["ignore", "pipe", "pipe"],
462
- timeout: 10000,
463
- });
464
- return "wrote";
493
+ const addUser = (value) => runClaude([
494
+ "mcp", "add-json", "-s", "user", "echomem", JSON.stringify(value),
495
+ ]);
496
+ const removeUser = () => runClaude(["mcp", "remove", "echomem", "-s", "user"]);
497
+ const before = readClaudeCodeConfigFile(configPath);
498
+ const previousUserEntry = echoMemEntryFromServers(before.mcpServers);
499
+ let restoredPreviousUserEntry = false;
500
+ // Avoid interrupting active/new sessions when the correct global entry is already installed.
501
+ if (!claudeEntriesMatch(previousUserEntry, entry)) {
502
+ if (previousUserEntry && !removeUser())
503
+ return emptyResult();
504
+ if (!addUser(entry)) {
505
+ if (previousUserEntry)
506
+ restoredPreviousUserEntry = addUser(previousUserEntry);
507
+ return { ...emptyResult(), restoredPreviousUserEntry };
508
+ }
465
509
  }
466
- catch {
467
- return "unavailable";
510
+ const installedUserEntry = echoMemEntryFromServers(readClaudeCodeConfigFile(configPath).mcpServers);
511
+ if (!claudeEntriesMatch(installedUserEntry, entry)) {
512
+ return { ...emptyResult(), restoredPreviousUserEntry };
513
+ }
514
+ const removedLocalProjects = [];
515
+ const skippedLocalProjects = [];
516
+ const failedLocalProjects = [];
517
+ for (const projectPath of claudeCodeLocalEchoMemProjects(configPath)) {
518
+ // A deleted directory cannot currently shadow user scope. Do not recreate it or hand-edit
519
+ // ~/.claude.json, which also contains Claude account/session state.
520
+ if (!fs.existsSync(projectPath)) {
521
+ skippedLocalProjects.push(projectPath);
522
+ continue;
523
+ }
524
+ let projectCwd = projectPath;
525
+ try {
526
+ projectCwd = fs.realpathSync(projectPath);
527
+ }
528
+ catch {
529
+ /* The existence check above already established the safe fallback path. */
530
+ }
531
+ if (runClaude(["mcp", "remove", "echomem", "-s", "local"], projectCwd)) {
532
+ removedLocalProjects.push(projectPath);
533
+ }
534
+ else {
535
+ failedLocalProjects.push(projectPath);
536
+ }
468
537
  }
538
+ const remainingActiveProjects = claudeCodeLocalEchoMemProjects(configPath)
539
+ .filter((projectPath) => fs.existsSync(projectPath));
540
+ const unresolved = [...new Set([...failedLocalProjects, ...remainingActiveProjects])].sort();
541
+ return {
542
+ state: unresolved.length > 0 ? "needs-repair" : "wrote",
543
+ removedLocalProjects,
544
+ skippedLocalProjects,
545
+ failedLocalProjects: unresolved,
546
+ restoredPreviousUserEntry,
547
+ };
469
548
  }
470
549
  function readJsonClientEntry(configPath) {
471
550
  try {
@@ -2668,6 +2747,7 @@ async function cmdSetup(flags) {
2668
2747
  const entry = buildServerEntry({ devEntryPath: typeof flags.dev === "string" ? flags.dev : undefined });
2669
2748
  const requested = typeof flags.client === "string" ? flags.client : undefined;
2670
2749
  const targets = selectSetupTargets(requested, Boolean(flags.all));
2750
+ const configurationFailures = [];
2671
2751
  if (targets.length === 0) {
2672
2752
  console.log("No client auto-detected. Add this MCP server entry manually:\n");
2673
2753
  console.log(JSON.stringify({ echomem: entry }, null, 2));
@@ -2688,15 +2768,31 @@ async function cmdSetup(flags) {
2688
2768
  }
2689
2769
  else {
2690
2770
  const result = c.id === "claude-code" ? writeClaudeCodeConfig(entry) : "unavailable";
2691
- if (result === "wrote") {
2771
+ if (result !== "unavailable" && result.state === "wrote") {
2692
2772
  console.log(`✅ Wrote EchoMem MCP entry to ${c.label} via \`claude mcp add-json\` — start a new Claude Code session to load it.`);
2773
+ if (result.removedLocalProjects.length > 0) {
2774
+ console.log(`✅ Removed ${result.removedLocalProjects.length} stale Claude Code project-local EchoMem ${result.removedLocalProjects.length === 1 ? "entry" : "entries"}.`);
2775
+ }
2776
+ if (result.skippedLocalProjects.length > 0) {
2777
+ console.log(`ℹ️ Ignored ${result.skippedLocalProjects.length} EchoMem local ${result.skippedLocalProjects.length === 1 ? "entry" : "entries"} for deleted project directories; they cannot shadow the user entry.`);
2778
+ }
2693
2779
  }
2694
2780
  else {
2695
- console.log(`ℹ️ ${c.label}: ${c.note}\n entry: ${JSON.stringify(entry)}`);
2781
+ const failedProjects = result === "unavailable" ? [] : result.failedLocalProjects;
2782
+ configurationFailures.push(failedProjects.length > 0
2783
+ ? `${c.label} still has project-local EchoMem overrides in: ${failedProjects.join(", ")}`
2784
+ : `${c.label} user-scoped EchoMem entry could not be verified`);
2696
2785
  }
2697
2786
  }
2698
2787
  }
2699
2788
  }
2789
+ if (configurationFailures.length > 0) {
2790
+ throw new Error([
2791
+ "EchoMem MCP configuration is incomplete; onboarding was stopped before login/import.",
2792
+ ...configurationFailures.map((failure) => `- ${failure}`),
2793
+ `Retry with: ${MCP_UPDATE_COMMAND} --client claude-code`,
2794
+ ].join("\n"));
2795
+ }
2700
2796
  if (!flags["no-agents-md"]) {
2701
2797
  writeMemoryGuidanceForTargets(targets);
2702
2798
  }
@@ -165,6 +165,42 @@ const triggerMetadataSchema = {
165
165
  triggerMessage: z.string().optional(),
166
166
  triggerMessageRole: z.string().optional(),
167
167
  };
168
+ // Canonical workspace selector for tools that act inside a company workspace.
169
+ // `workspaceId` is the current name; `groupId` is the legacy alias kept working
170
+ // so existing prompts keep functioning. Handlers normalize with
171
+ // normalizeWorkspaceId (workspaceId ?? groupId). Required only when the user
172
+ // belongs to more than one workspace; a single-workspace user may omit it.
173
+ const WORKSPACE_SELECTOR_DESCRIPTION = "Workspace to act in. Required when the user belongs to more than one workspace; omit it when they have a single workspace. If omitted with multiple workspaces, the tool returns the available workspaces so you can ask the user which to use.";
174
+ const workspaceSelectorSchema = {
175
+ workspaceId: z.string().uuid().optional().describe(WORKSPACE_SELECTOR_DESCRIPTION),
176
+ groupId: z.string().uuid().optional().describe("Legacy alias for workspaceId."),
177
+ };
178
+ // The advertised JSON-Schema counterpart of workspaceSelectorSchema. Tool
179
+ // inputSchemas are hand-written, so the selector must be injected into every
180
+ // workspace-scoped tool's properties or clients never learn they can pass it.
181
+ const workspaceSelectorProperties = {
182
+ workspaceId: { type: "string", description: WORKSPACE_SELECTOR_DESCRIPTION },
183
+ groupId: { type: "string", description: "Legacy alias for workspaceId." },
184
+ };
185
+ const WORKSPACE_SCOPED_TOOL_NAMES = new Set([
186
+ canonicalToolNames.others,
187
+ canonicalToolNames.publicMemory,
188
+ canonicalToolNames.groupContext,
189
+ canonicalToolNames.createGroupInvite,
190
+ canonicalToolNames.prepareGroupPublication,
191
+ canonicalToolNames.updateGroupProfile,
192
+ canonicalToolNames.publishToGroup,
193
+ canonicalToolNames.publishBatchToGroup,
194
+ ]);
195
+ function injectWorkspaceSelector(specs) {
196
+ for (const spec of specs) {
197
+ if (!WORKSPACE_SCOPED_TOOL_NAMES.has(spec.name))
198
+ continue;
199
+ const existing = spec.inputSchema.properties ?? {};
200
+ spec.inputSchema.properties = { ...workspaceSelectorProperties, ...existing };
201
+ }
202
+ return specs;
203
+ }
168
204
  export const searchMemoriesSchema = z.object({
169
205
  ...triggerMetadataSchema,
170
206
  query: z.string().trim().min(1).optional(),
@@ -235,6 +271,8 @@ export const sendFriendRequestSchema = z.object({
235
271
  });
236
272
  export const othersSchema = z.object({
237
273
  ...triggerMetadataSchema,
274
+ ...workspaceSelectorSchema,
275
+ scope: z.enum(["group", "friends"]).optional(),
238
276
  query: z.string().trim().optional().default(""),
239
277
  limit: z.number().int().min(1).max(50).optional().default(10),
240
278
  target: z.string().optional(),
@@ -249,6 +287,7 @@ export const othersSchema = z.object({
249
287
  });
250
288
  export const publicMemorySchema = z.object({
251
289
  ...triggerMetadataSchema,
290
+ ...workspaceSelectorSchema,
252
291
  memoryId: z.string().min(1),
253
292
  });
254
293
  export const recordMemoryCitationsSchema = z.object({
@@ -258,6 +297,7 @@ export const recordMemoryCitationsSchema = z.object({
258
297
  });
259
298
  export const groupContextSchema = z.object({
260
299
  ...triggerMetadataSchema,
300
+ ...workspaceSelectorSchema,
261
301
  });
262
302
  export const getGroupSessionSharingSchema = z.object({
263
303
  ...triggerMetadataSchema,
@@ -282,6 +322,7 @@ export const createGroupSchema = z.object({
282
322
  });
283
323
  export const createGroupInviteSchema = z.object({
284
324
  ...triggerMetadataSchema,
325
+ ...workspaceSelectorSchema,
285
326
  expiresInDays: z.number().int().min(1).max(30).optional(),
286
327
  maxUses: z.number().int().min(1).max(100).optional(),
287
328
  });
@@ -294,6 +335,7 @@ export const joinGroupSchema = z.object({
294
335
  });
295
336
  export const prepareGroupPublicationSchema = z.object({
296
337
  ...triggerMetadataSchema,
338
+ ...workspaceSelectorSchema,
297
339
  scope: z.enum(["bootstrap", "since_last_scan", "context", "time_range"]).default("since_last_scan"),
298
340
  contextId: z.string().min(1).optional(),
299
341
  startAt: z.string().optional(),
@@ -310,6 +352,7 @@ export const flagPublicationAttentionSchema = z.object({
310
352
  });
311
353
  export const updateGroupProfileSchema = z.object({
312
354
  ...triggerMetadataSchema,
355
+ ...workspaceSelectorSchema,
313
356
  displayName: z.string().min(1).max(120).optional(),
314
357
  title: z.string().min(1).max(160),
315
358
  responsibilitySummary: z.string().min(1).max(1000),
@@ -325,11 +368,13 @@ export const completeGroupPublicationSchema = z.object({
325
368
  });
326
369
  export const publishToGroupSchema = z.object({
327
370
  ...triggerMetadataSchema,
371
+ ...workspaceSelectorSchema,
328
372
  memoryId: z.string().min(1),
329
373
  acknowledgedFlaggedMemoryIds: z.array(z.string().min(1)).max(1).optional(),
330
374
  });
331
375
  export const publishBatchToGroupSchema = z.object({
332
376
  ...triggerMetadataSchema,
377
+ ...workspaceSelectorSchema,
333
378
  memoryIds: z.array(z.string().min(1)).min(1).max(50),
334
379
  contextId: z.string().min(1).optional(),
335
380
  selectionReason: z.string().max(500).optional(),
@@ -573,10 +618,15 @@ export function listToolSpecs(opts = {}) {
573
618
  {
574
619
  name: canonicalToolNames.others,
575
620
  title: "Search teammates' and friends' memories",
576
- description: `PEER-MEMORY SEARCH for public memories owned by accepted friends or company-group members—not the user's own memories. Pass query for a topic; omit it only when intentionally browsing peer memories, and optionally use target to scope a person. Do not use this tool for the user's private memories or the EchoMem user directory. EchoMem identifies the caller from the EchoMem credential and has already excluded only that authenticated user's own memories. Present every returned owner; never filter again using a Claude account, host profile, git identity, or inference. For onboarding and division-of-work questions, call get_group_context first. Returned memories are recorded in memory_views for the owners. ${memoryCitationInstruction}${groupMapSection}`,
621
+ description: `PEER-MEMORY SEARCH for public memories owned by accepted friends or company-group members—not the user's own memories. Pass query for a topic. Set scope to pick the audience — 'group' (only workspace teammates) or 'friends' (only friends) or target for one person; scope defaults to teammates. Do not use this tool for the user's private memories or the EchoMem user directory. EchoMem identifies the caller from the EchoMem credential and has already excluded only that authenticated user's own memories. Present every returned owner; never filter again using a Claude account, host profile, git identity, or inference. For onboarding and division-of-work questions, call get_group_context first. Returned memories are recorded in memory_views for the owners. ${memoryCitationInstruction}${groupMapSection}`,
577
622
  inputSchema: {
578
623
  type: "object",
579
624
  properties: {
625
+ scope: {
626
+ type: "string",
627
+ enum: ["group", "friends"],
628
+ description: "Audience to search. 'group' = only the named workspace's teammates (you'll be asked which if the user is in several and names none). 'friends' = only accepted friends. For one specific person, omit scope and pass target instead. Omitting scope defaults to the group/teammates audience.",
629
+ },
580
630
  query: {
581
631
  type: "string",
582
632
  description: "Optional peer-memory topic. Omit only for an intentional broad browse; never send this field as conversation.",
@@ -584,7 +634,7 @@ export function listToolSpecs(opts = {}) {
584
634
  limit: { type: "integer", minimum: 1, maximum: 50, default: 10 },
585
635
  target: {
586
636
  type: "string",
587
- description: "Accessible friend or group-member user id or exact display name. Prefer this for @Name asks.",
637
+ description: "Accessible friend or group-member user id or exact display name. Prefer this for @Name asks or a single specific person.",
588
638
  },
589
639
  ownerUserId: {
590
640
  type: "string",
@@ -1021,5 +1071,5 @@ export function listToolSpecs(opts = {}) {
1021
1071
  },
1022
1072
  },
1023
1073
  ];
1024
- return tools.map(decorateLocalToolSpec);
1074
+ return injectWorkspaceSelector(tools).map(decorateLocalToolSpec);
1025
1075
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.41",
3
+ "version": "1.4.42",
4
4
  "description": "EchoMem MCP bridge: cloud-first memory tools and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "test:ui": "npm run build && node test/setup-ui.test.mjs",
34
34
  "test:onboarding-resilience": "npm run build && node test/onboarding-resilience.test.mjs",
35
35
  "test:billing-ui": "npm run build && node test/setup-ui.test.mjs billing",
36
- "test": "npm run build && node test/source-session.test.mjs && node test/source-session-hook.test.mjs && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/onboarding-resilience.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/group-map.test.mjs && node test/update-check.test.mjs && node test/headless-runtime.test.mjs && node test/claude-code-config.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/save-checkpoint-hook.test.mjs",
36
+ "test": "npm run build && node test/source-session.test.mjs && node test/source-session-hook.test.mjs && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/onboarding-resilience.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/workspace-selector.test.mjs && node test/group-map.test.mjs && node test/update-check.test.mjs && node test/headless-runtime.test.mjs && node test/claude-code-config.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/save-checkpoint-hook.test.mjs",
37
37
  "prepack": "npm run build && node scripts/bundle-city.mjs"
38
38
  },
39
39
  "dependencies": {