@echomem/mcp 1.4.51 โ†’ 1.4.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -133,6 +133,20 @@ Both hook entries are merged idempotently into `~/.codex/hooks.json` and
133
133
  `~/.claude/settings.json`; unrelated user hooks and settings are preserved. Hooks fail open when
134
134
  their local input is missing so they cannot prevent a conversation from starting or finishing.
135
135
 
136
+ ### Cloud Cowork through `remote-devices`
137
+
138
+ Cloud Cowork inference cannot use the local hook or transcript-verification path. The shared local
139
+ MCP process also must not retain one cloud conversation as global bridge state. Cloud Cowork agents
140
+ therefore pass their runtime-issued `session_...` identifier as `cloudCoworkSessionId` on each
141
+ EchoMem tool call. EchoMem binds that request to the canonical `cowork:session_...` context with
142
+ `cloud_cowork_tool_argument` evidence and `remote_mcp` origin, then links or saves against that
143
+ context without changing shared bridge state.
144
+
145
+ This identity is intentionally reported as **provisional**: the authenticated user owns the
146
+ context and ticket link, but EchoMem cannot independently corroborate the cloud identifier from a
147
+ local transcript. Verified local Cowork remains `cowork:local_...` with trusted local evidence.
148
+ Never convert one form into the other or silently present a provisional cloud link as verified.
149
+
136
150
  ### Manual / headless (SSH, containers, CI)
137
151
 
138
152
  No browser? Provide secrets directly โ€” this is the documented headless path:
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS, MEMORY_CITATION_INSTRUCTI
16
16
  import { clearBillingAlert, writeBillingAlert } from "./billing-alert.js";
17
17
  import { checkLatestUpdateStatus, formatUpdateNotice, formatUpdateStatusText, startBackgroundUpdateCheck, } from "./update-check.js";
18
18
  import { autoUpdateHeadlessRuntime } from "./headless-runtime.js";
19
- import { resolveSourceSessionRequestContext, resolveSourceSessionFromBindingToken, } from "./source-session.js";
19
+ import { SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE, resolveCloudCoworkSessionFromToolArguments, resolveSourceSessionRequestContext, resolveSourceSessionFromBindingToken, } from "./source-session.js";
20
20
  const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
21
21
  const ECHO_PRICING_URL = process.env.ECHO_PRICING_URL || "https://echoknows.com/account";
22
22
  const ECHO_MEMORY_WEB_URL = (process.env.ECHO_MEMORY_WEB_URL || "https://echoknows.com/memory").replace(/\/$/, "");
@@ -169,6 +169,17 @@ function formatReconnectRequiredResult(error) {
169
169
  "No editor restart is needed. Keep all credentials out of chat.",
170
170
  ].join("\n");
171
171
  }
172
+ function formatWorkspaceTicketNotFoundResult(error) {
173
+ if (!axios.isAxiosError(error) || error.response?.status !== 404)
174
+ return null;
175
+ if (errorCodeFrom(error.response.data) !== "WORKSPACE_TICKET_NOT_FOUND")
176
+ return null;
177
+ return [
178
+ "๐ŸŽซ This Echo workspace ticket no longer exists or is unavailable.",
179
+ "Do not retry this ticket ID.",
180
+ "Ask the user whether to continue without a ticket or attach this session to another ticket.",
181
+ ].join("\n");
182
+ }
172
183
  /** Map a thrown error to the telemetry error_kind taxonomy. */
173
184
  function classifyError(error) {
174
185
  if (error instanceof NoTokenError)
@@ -339,6 +350,9 @@ function detectMcpHostFromEnv() {
339
350
  return "cursor";
340
351
  if (process.env.WINDSURF_WORKSPACE_ID || process.env.WINDSURF_USER_ID)
341
352
  return "windsurf";
353
+ // Cowork embeds a Claude Code process, but this outer host ID is the distinguishing signal.
354
+ if (process.env.CLAUDE_CODE_HOST_SESSION_ID)
355
+ return "cowork";
342
356
  if (process.env.CLAUDE_CODE || process.env.CLAUDECODE || process.env.ANTHROPIC_CLAUDE_CODE) {
343
357
  return "claude_code";
344
358
  }
@@ -697,7 +711,7 @@ class EchoMemApiClient {
697
711
  const requestContext = this.requestContext.getStore();
698
712
  return requestContext === undefined ? this.boundSourceSession : requestContext.sourceSession;
699
713
  }
700
- getRequestVerifiedSourceSession() {
714
+ getRequestSourceSession() {
701
715
  const requestContext = this.requestContext.getStore();
702
716
  if (!requestContext || requestContext.lineageStatus === "unbound")
703
717
  return null;
@@ -709,26 +723,29 @@ class EchoMemApiClient {
709
723
  async withRequestContext(requestContext, operation) {
710
724
  return this.requestContext.run(requestContext, operation);
711
725
  }
712
- async bindSourceSession(verified, persistForBridge = true) {
726
+ async bindSourceSession(identity, persistForBridge = true) {
713
727
  this.synchronizeAccountContext();
714
- const cached = this.sourceSessionsByCanonicalKey.get(verified.canonicalKey);
728
+ if (identity.evidence === SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE && persistForBridge) {
729
+ throw new Error("A provisional Cloud Cowork identity cannot become shared bridge state");
730
+ }
731
+ const cached = this.sourceSessionsByCanonicalKey.get(identity.canonicalKey);
715
732
  if (cached) {
716
733
  if (persistForBridge)
717
734
  this.boundSourceSession = cached;
718
735
  return { ...cached, created: false };
719
736
  }
720
737
  const response = await this.axios.post("/api/extension/source-sessions/bind", {
721
- provider: verified.provider,
722
- providerSessionId: verified.providerSessionId,
723
- evidence: verified.evidence,
738
+ provider: identity.provider,
739
+ providerSessionId: identity.providerSessionId,
740
+ evidence: identity.evidence,
724
741
  });
725
742
  const contextId = readString(response.data, "contextId");
726
743
  const canonicalKey = readString(response.data, "canonicalKey");
727
- if (response.data?.success !== true || !contextId || canonicalKey !== verified.canonicalKey) {
744
+ if (response.data?.success !== true || !contextId || canonicalKey !== identity.canonicalKey) {
728
745
  throw new Error("EchoMem returned an invalid source-session binding receipt");
729
746
  }
730
- const bound = { ...verified, contextId };
731
- this.sourceSessionsByCanonicalKey.set(verified.canonicalKey, bound);
747
+ const bound = { ...identity, contextId };
748
+ this.sourceSessionsByCanonicalKey.set(identity.canonicalKey, bound);
732
749
  if (this.sourceSessionsByCanonicalKey.size > 64) {
733
750
  const oldestKey = this.sourceSessionsByCanonicalKey.keys().next().value;
734
751
  if (oldestKey)
@@ -745,9 +762,10 @@ class EchoMemApiClient {
745
762
  const parsed = linkWorkspaceTicketSessionSchema.parse(args ?? {});
746
763
  // A bridge-level compatibility binding can outlive a conversation in
747
764
  // long-running hosts. Ticket links are therefore allowed to take the fast
748
- // path only from identity verified for this exact request; otherwise the
749
- // Desktop transcript watcher completes the link from local evidence.
750
- const sourceSession = this.getRequestVerifiedSourceSession();
765
+ // path only from identity scoped to this exact request. That identity is
766
+ // either verified host metadata or an explicitly marked provisional Cloud
767
+ // Cowork claim; otherwise Desktop completes the link from local evidence.
768
+ const sourceSession = this.getRequestSourceSession();
751
769
  if (!sourceSession)
752
770
  return null;
753
771
  const response = await this.axios.post(`/api/extension/workspace-tickets/${encodeURIComponent(parsed.ticketId)}/sessions`, {
@@ -1026,10 +1044,14 @@ class EchoMemApiClient {
1026
1044
  // For an encrypted account, hand the server the key transiently in the X-Encryption-Key header
1027
1045
  // so it encrypts at rest (mirrors the extension's write path, spec ยง3.1a). Locked โ†’ LockedError.
1028
1046
  const enc = await this.encState();
1047
+ const boundSourceSession = this.getBoundSourceSession();
1048
+ const originChannel = boundSourceSession?.evidence === SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE
1049
+ ? "remote_mcp"
1050
+ : "local_mcp";
1029
1051
  const config = {
1030
1052
  headers: {
1031
1053
  "X-EchoMem-Request-Id": randomUUID(),
1032
- "X-EchoMem-Origin-Channel": "local_mcp",
1054
+ "X-EchoMem-Origin-Channel": originChannel,
1033
1055
  ...(enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : {}),
1034
1056
  },
1035
1057
  };
@@ -1039,9 +1061,10 @@ class EchoMemApiClient {
1039
1061
  sourceUrl: parsed.url,
1040
1062
  source: parsed.source || "mcp_server",
1041
1063
  title: parsed.title,
1042
- // A verified source session owns the memory context. The independent opaque scope still
1064
+ // A source-session identity owns the memory context. Provisional Cloud Cowork identity
1065
+ // is explicitly marked by its evidence and origin channel. The independent opaque scope still
1043
1066
  // governs group-sharing consent and never becomes the source-session identity.
1044
- conversationKey: this.getBoundSourceSession()?.canonicalKey ?? groupSharingScopeId,
1067
+ conversationKey: boundSourceSession?.canonicalKey ?? groupSharingScopeId,
1045
1068
  groupSharingScopeId,
1046
1069
  passthrough: parsed.passthrough || false,
1047
1070
  triggerMessage: parsed.triggerMessage ||
@@ -1411,8 +1434,10 @@ class EchoMemMCPServer {
1411
1434
  });
1412
1435
  }
1413
1436
  getMcpClientAnalytics() {
1414
- const hostPlatform = normalizeMcpHostPlatform(this.mcpClientName)
1415
- ?? detectMcpHostFromEnv()
1437
+ const environmentPlatform = detectMcpHostFromEnv();
1438
+ const hostPlatform = (environmentPlatform === "cowork" ? environmentPlatform : undefined)
1439
+ ?? normalizeMcpHostPlatform(this.mcpClientName)
1440
+ ?? environmentPlatform
1416
1441
  ?? "unknown";
1417
1442
  return {
1418
1443
  mcp_client_name: this.mcpClientName,
@@ -1486,6 +1511,23 @@ class EchoMemMCPServer {
1486
1511
  catch {
1487
1512
  // Malformed host metadata is not model input. Ignore it and retain the explicit fallback.
1488
1513
  }
1514
+ let provisionalCloudSession = null;
1515
+ try {
1516
+ provisionalCloudSession = resolveCloudCoworkSessionFromToolArguments(toolArgs);
1517
+ }
1518
+ catch (error) {
1519
+ throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : "Invalid Cloud Cowork session identity");
1520
+ }
1521
+ if (provisionalCloudSession && sourceResolution.sourceSession) {
1522
+ throw new McpError(ErrorCode.InvalidParams, "This request already has a verified local source session. Omit cloudCoworkSessionId rather than mixing identities.");
1523
+ }
1524
+ if (provisionalCloudSession) {
1525
+ sourceResolution = {
1526
+ sourceSession: provisionalCloudSession,
1527
+ isSubagent: false,
1528
+ lineageStatus: "not_applicable",
1529
+ };
1530
+ }
1489
1531
  // A child request starts fail-closed. Resolved children bind to the originating root; unresolved
1490
1532
  // children carry an explicit null so AsyncLocalStorage cannot fall through to a global binding.
1491
1533
  let requestSourceSession = sourceResolution.isSubagent
@@ -1735,6 +1777,13 @@ class EchoMemMCPServer {
1735
1777
  isError: true,
1736
1778
  };
1737
1779
  }
1780
+ const missingWorkspaceTicket = formatWorkspaceTicketNotFoundResult(error);
1781
+ if (missingWorkspaceTicket) {
1782
+ return {
1783
+ content: [{ type: "text", text: missingWorkspaceTicket }],
1784
+ isError: true,
1785
+ };
1786
+ }
1738
1787
  const upgradeRequired = formatUpgradeRequiredResult(error);
1739
1788
  if (upgradeRequired) {
1740
1789
  return {
@@ -2028,15 +2077,26 @@ Details: ${m.details || "N/A"}`)
2028
2077
  }
2029
2078
  const ticket = isRecord(result.ticket) ? result.ticket : {};
2030
2079
  const workspaceId = readString(ticket, "workspaceId") ?? parsed.workspaceId;
2080
+ const sourceSession = this.client.getBoundSourceSession();
2081
+ const provisional = sourceSession?.evidence === SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE;
2031
2082
  return {
2032
2083
  content: [{
2033
2084
  type: "text",
2034
2085
  text: [
2035
2086
  result.changed === true
2036
- ? "Linked this verified source session to the Echo workspace ticket."
2037
- : "This verified source session was already linked to the Echo workspace ticket.",
2087
+ ? provisional
2088
+ ? "Linked this provisional Cloud Cowork session to the Echo workspace ticket."
2089
+ : "Linked this verified source session to the Echo workspace ticket."
2090
+ : provisional
2091
+ ? "This provisional Cloud Cowork session was already linked to the Echo workspace ticket."
2092
+ : "This verified source session was already linked to the Echo workspace ticket.",
2038
2093
  `Ticket: ${parsed.ticketId}`,
2039
2094
  workspaceId ? `Workspace: ${workspaceId}` : "",
2095
+ sourceSession ? `Source session: ${sourceSession.canonicalKey}` : "",
2096
+ sourceSession ? `Context: ${sourceSession.contextId}` : "",
2097
+ provisional
2098
+ ? "Identity trust: provisional (reported by Cloud Cowork in the tool call; not verified by a local transcript). Repeat cloudCoworkSessionId on later EchoMem calls from this conversation."
2099
+ : "",
2040
2100
  "Retries are safe and do not create duplicate links or history events.",
2041
2101
  ].filter(Boolean).join("\n"),
2042
2102
  }],
@@ -4,6 +4,7 @@ import { resolveClaudeCoworkSessionsDir, resolveClaudeProjectsDir, resolveCodexS
4
4
  export const SOURCE_SESSION_BINDING_EVIDENCE = "local_jsonl_tool_call";
5
5
  export const SOURCE_SESSION_HOOK_EVIDENCE = "local_session_start_hook";
6
6
  export const SOURCE_SESSION_MCP_METADATA_EVIDENCE = "local_mcp_session_metadata";
7
+ export const SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE = "cloud_cowork_tool_argument";
7
8
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8
9
  const UUID_IN_TEXT_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
9
10
  const MAX_TAIL_BYTES = 512 * 1024;
@@ -20,13 +21,18 @@ function normalizedProviderSessionId(provider, value) {
20
21
  if (provider === "codex" && !UUID_RE.test(normalized)) {
21
22
  throw new Error("Codex source-session ID must be a UUID");
22
23
  }
23
- if (provider === "cowork" && !/^local_[A-Za-z0-9_-]{1,194}$/.test(normalized)) {
24
- throw new Error("Cowork source-session ID must use the local_ host session identifier");
24
+ if (provider === "cowork"
25
+ && !/^local_[A-Za-z0-9_-]{1,194}$/.test(normalized)
26
+ && !/^session_[A-Za-z0-9_-]{1,192}$/.test(normalized)) {
27
+ throw new Error("Cowork source-session ID must use a local_ host ID or session_ Cloud Cowork ID");
25
28
  }
26
29
  return UUID_RE.test(normalized) ? normalized.toLowerCase() : normalized;
27
30
  }
28
31
  export function verifiedSourceSession(provider, providerSessionId, evidence) {
29
32
  const normalized = normalizedProviderSessionId(provider, providerSessionId);
33
+ if (provider === "cowork" && !normalized.startsWith("local_")) {
34
+ throw new Error("Verified Cowork source-session ID must use the local_ host identifier");
35
+ }
30
36
  return {
31
37
  provider,
32
38
  providerSessionId: normalized,
@@ -34,6 +40,26 @@ export function verifiedSourceSession(provider, providerSessionId, evidence) {
34
40
  evidence,
35
41
  };
36
42
  }
43
+ export function provisionalCloudCoworkSession(providerSessionId) {
44
+ const normalized = normalizedProviderSessionId("cowork", providerSessionId);
45
+ if (!normalized.startsWith("session_")) {
46
+ throw new Error("Cloud Cowork source-session ID must use the session_ identifier from the cloud runtime");
47
+ }
48
+ return {
49
+ provider: "cowork",
50
+ providerSessionId: normalized,
51
+ canonicalKey: `cowork:${normalized}`,
52
+ evidence: SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE,
53
+ };
54
+ }
55
+ export function resolveCloudCoworkSessionFromToolArguments(args) {
56
+ if (!isRecord(args) || args.cloudCoworkSessionId === undefined)
57
+ return null;
58
+ if (typeof args.cloudCoworkSessionId !== "string") {
59
+ throw new Error("cloudCoworkSessionId must be the current Cloud Cowork session_ identifier");
60
+ }
61
+ return provisionalCloudCoworkSession(args.cloudCoworkSessionId);
62
+ }
37
63
  function isRecord(value) {
38
64
  return typeof value === "object" && value !== null && !Array.isArray(value);
39
65
  }
@@ -361,14 +387,23 @@ export function resolveSourceSessionRequestContext(metadata, options = {}) {
361
387
  agentDepth: lineage.agentDepth,
362
388
  };
363
389
  }
390
+ const isClaudeHost = host.includes("claude") || host.includes("cowork");
391
+ const coworkCandidate = env.CLAUDE_CODE_HOST_SESSION_ID?.trim() ?? "";
392
+ // Cowork embeds Claude Code, so its MCP handshake may still identify the nested client as
393
+ // "Claude Code" and expose both IDs. The outer host session is the durable Cowork conversation;
394
+ // it must win over the nested CLI session whenever Anthropic supplies both host-owned values.
395
+ if (isClaudeHost && coworkCandidate) {
396
+ return {
397
+ sourceSession: verifiedSourceSession("cowork", coworkCandidate, SOURCE_SESSION_MCP_METADATA_EVIDENCE),
398
+ isSubagent: false,
399
+ lineageStatus: "not_applicable",
400
+ };
401
+ }
364
402
  if (host.includes("claude_desktop") || host === "claude" || host.includes("cowork")) {
365
- const candidate = env.CLAUDE_CODE_HOST_SESSION_ID?.trim() ?? "";
366
403
  return {
367
- sourceSession: candidate
368
- ? verifiedSourceSession("cowork", candidate, SOURCE_SESSION_MCP_METADATA_EVIDENCE)
369
- : null,
404
+ sourceSession: null,
370
405
  isSubagent: false,
371
- lineageStatus: candidate ? "not_applicable" : "unbound",
406
+ lineageStatus: "unbound",
372
407
  };
373
408
  }
374
409
  if (host.includes("claude_code")) {
@@ -384,7 +419,10 @@ export function resolveSourceSessionRequestContext(metadata, options = {}) {
384
419
  return { sourceSession: null, isSubagent: false, lineageStatus: "unbound" };
385
420
  }
386
421
  export function resolveSourceSessionFromMcpContext(metadata, options = {}) {
387
- return resolveSourceSessionRequestContext(metadata, options).sourceSession;
422
+ const sourceSession = resolveSourceSessionRequestContext(metadata, options).sourceSession;
423
+ return sourceSession?.evidence === SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE
424
+ ? null
425
+ : sourceSession;
388
426
  }
389
427
  export function resolveSourceSessionFromBindingToken(bindingToken, options = {}) {
390
428
  const normalizedToken = bindingToken.trim().toLowerCase();
@@ -164,7 +164,32 @@ function decorateLocalToolSpec(tool) {
164
164
  const triggerMetadataSchema = {
165
165
  triggerMessage: z.string().optional(),
166
166
  triggerMessageRole: z.string().optional(),
167
+ cloudCoworkSessionId: z.string().regex(/^session_[A-Za-z0-9_-]{1,192}$/).optional(),
167
168
  };
169
+ const CLOUD_COWORK_SESSION_DESCRIPTION = "Cloud Cowork only: the current session_ identifier supplied by the Cowork runtime. Repeat it on each EchoMem call so the shared remote-devices bridge can attach that one request to the same provisional conversation context. Never guess or reuse an ID from another conversation. Local Cowork, Claude Code, and Codex must omit this field.";
170
+ const CLOUD_COWORK_SESSION_PROPERTY = {
171
+ type: "string",
172
+ pattern: "^session_[A-Za-z0-9_-]{1,192}$",
173
+ description: CLOUD_COWORK_SESSION_DESCRIPTION,
174
+ };
175
+ const SOURCE_CONTEXT_FREE_TOOL_NAMES = new Set([
176
+ canonicalToolNames.bindSourceSession,
177
+ canonicalToolNames.updateStatus,
178
+ canonicalToolNames.contextHealth,
179
+ canonicalToolNames.recompose,
180
+ ]);
181
+ function injectCloudCoworkSession(specs) {
182
+ for (const spec of specs) {
183
+ if (SOURCE_CONTEXT_FREE_TOOL_NAMES.has(spec.name))
184
+ continue;
185
+ const existing = spec.inputSchema.properties ?? {};
186
+ spec.inputSchema.properties = {
187
+ ...existing,
188
+ cloudCoworkSessionId: CLOUD_COWORK_SESSION_PROPERTY,
189
+ };
190
+ }
191
+ return specs;
192
+ }
168
193
  // Canonical workspace selector for tools that act inside a company workspace.
169
194
  // `workspaceId` is the current name; `groupId` is the legacy alias kept working
170
195
  // so existing prompts keep functioning. Handlers normalize with
@@ -478,7 +503,7 @@ export function listToolSpecs(opts = {}) {
478
503
  {
479
504
  name: canonicalToolNames.linkWorkspaceTicketSession,
480
505
  title: "Link this agent conversation to an Echo workspace ticket",
481
- description: withMcpVersion("Call when the user asks to work on, continue, attach, or bind this conversation to an Echo workspace ticket and supplies its UUID. Pass the ticketId exactly; pass workspaceId when a structured Echo ticket marker provides it. The operation is idempotent. If the current source session is already verified, EchoMem links it immediately. Otherwise Echo Desktop can recover this exact tool invocation from the local transcript and finish the verified link; never guess or ask the user for a source-session/context ID."),
506
+ description: withMcpVersion("Call when the user asks to work on, continue, attach, or bind this conversation to an Echo workspace ticket and supplies its UUID. Pass the ticketId exactly; pass workspaceId when a structured Echo ticket marker provides it. The operation is idempotent. A verified local source session links immediately. Cloud Cowork has no local transcript, so pass its current session_ runtime identifier as cloudCoworkSessionId; EchoMem records that path explicitly as provisional and links it immediately. Otherwise Echo Desktop can recover this exact invocation from a local transcript. Never guess or ask the user for a source-session/context ID."),
482
507
  inputSchema: {
483
508
  type: "object",
484
509
  properties: {
@@ -1097,5 +1122,5 @@ export function listToolSpecs(opts = {}) {
1097
1122
  },
1098
1123
  },
1099
1124
  ];
1100
- return injectWorkspaceSelector(tools).map(decorateLocalToolSpec);
1125
+ return injectWorkspaceSelector(injectCloudCoworkSession(tools)).map(decorateLocalToolSpec);
1101
1126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.51",
3
+ "version": "1.4.53",
4
4
  "description": "EchoMem MCP bridge for cross-agent memory, local history import, and recall",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "test:billing-ui": "npm run build && node test/setup-ui.test.mjs billing",
34
34
  "test:config-safety": "npm run build && node test/config-safety.test.mjs",
35
35
  "test:mcp-control": "npm run build && node test/mcp-control.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/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/config-safety.test.mjs && node test/mcp-control.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 && node test/durable-entry.test.mjs && node test/durable-reexec.test.mjs && node test/doctor.test.mjs && node test/environment-matrix.test.mjs",
36
+ "test": "npm run build && node test/source-session.test.mjs && node test/cowork-ticket-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/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/config-safety.test.mjs && node test/mcp-control.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 && node test/durable-entry.test.mjs && node test/durable-reexec.test.mjs && node test/doctor.test.mjs && node test/environment-matrix.test.mjs",
37
37
  "prepack": "npm run build"
38
38
  },
39
39
  "dependencies": {