@echomem/mcp 1.4.49 → 1.4.51

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.
@@ -9,6 +9,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
9
9
  const MAX_TAIL_BYTES = 512 * 1024;
10
10
  const DEFAULT_MAX_AGE_MS = 15 * 60 * 1000;
11
11
  const MAX_RECENT_FILES_PER_PROVIDER = 24;
12
+ const MAX_CODEX_AGENT_DEPTH = 16;
13
+ const CODEX_FILE_INDEX_TTL_MS = 5_000;
14
+ const codexFileIndexes = new Map();
12
15
  function normalizedProviderSessionId(provider, value) {
13
16
  const normalized = value.trim();
14
17
  if (!normalized || normalized.length > 200 || /\s/.test(normalized)) {
@@ -31,47 +34,73 @@ export function verifiedSourceSession(provider, providerSessionId, evidence) {
31
34
  evidence,
32
35
  };
33
36
  }
34
- /**
35
- * Resolve identity from host-owned MCP metadata. Unlike tool arguments, these values are attached by
36
- * Codex/Claude outside the model-controlled input. The SessionStart hook performs the eager bind;
37
- * this resolver lets the MCP process independently attach the same identity on its first real call.
38
- */
39
- export function resolveSourceSessionFromMcpContext(metadata, options = {}) {
40
- const host = options.hostPlatform?.toLowerCase().replace(/[^a-z0-9]+/g, "_") ?? "";
41
- const env = options.env ?? process.env;
42
- if (host.includes("codex")) {
43
- const root = isRecord(metadata) ? metadata : {};
44
- const turn = isRecord(root["x-codex-turn-metadata"])
45
- ? root["x-codex-turn-metadata"]
46
- : {};
47
- const candidate = typeof turn.thread_id === "string"
48
- ? turn.thread_id
49
- : typeof turn.session_id === "string"
50
- ? turn.session_id
51
- : typeof root.threadId === "string"
52
- ? root.threadId
53
- : "";
54
- return candidate
55
- ? verifiedSourceSession("codex", candidate, SOURCE_SESSION_MCP_METADATA_EVIDENCE)
56
- : null;
37
+ function isRecord(value) {
38
+ return typeof value === "object" && value !== null && !Array.isArray(value);
39
+ }
40
+ function normalizedUuid(value) {
41
+ if (typeof value !== "string")
42
+ return null;
43
+ const normalized = value.trim().toLowerCase();
44
+ return UUID_RE.test(normalized) ? normalized : null;
45
+ }
46
+ function sourceParentThreadId(source) {
47
+ if (!isRecord(source) || !isRecord(source.subagent))
48
+ return null;
49
+ const direct = normalizedUuid(source.subagent.parent_thread_id);
50
+ if (direct)
51
+ return direct;
52
+ const spawn = isRecord(source.subagent.thread_spawn) ? source.subagent.thread_spawn : null;
53
+ return normalizedUuid(spawn?.parent_thread_id);
54
+ }
55
+ function metadataMarksSubagent(value) {
56
+ if (value == null || value === false || value === 0)
57
+ return false;
58
+ if (typeof value === "string") {
59
+ const normalized = value.trim().toLowerCase();
60
+ return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no";
57
61
  }
58
- if (host.includes("claude_desktop") || host === "claude" || host.includes("cowork")) {
59
- const candidate = env.CLAUDE_CODE_HOST_SESSION_ID?.trim() ?? "";
60
- return candidate
61
- ? verifiedSourceSession("cowork", candidate, SOURCE_SESSION_MCP_METADATA_EVIDENCE)
62
- : null;
62
+ return true;
63
+ }
64
+ function codexLineageNode(file) {
65
+ let descriptor = null;
66
+ try {
67
+ descriptor = fs.openSync(file, "r");
68
+ const stat = fs.fstatSync(descriptor);
69
+ const bytes = Math.min(stat.size, 128 * 1024);
70
+ const buffer = Buffer.allocUnsafe(bytes);
71
+ fs.readSync(descriptor, buffer, 0, bytes, 0);
72
+ for (const line of buffer.toString("utf8").split(/\r?\n/)) {
73
+ if (!line.includes("session_meta"))
74
+ continue;
75
+ try {
76
+ const row = JSON.parse(line);
77
+ if (!isRecord(row) || row.type !== "session_meta" || !isRecord(row.payload))
78
+ continue;
79
+ const id = normalizedUuid(row.payload.id) ?? normalizedUuid(row.payload.session_id);
80
+ if (!id)
81
+ return null;
82
+ const source = row.payload.source;
83
+ const directParent = normalizedUuid(row.payload.parent_thread_id);
84
+ const parentThreadId = directParent ?? sourceParentThreadId(source);
85
+ const isSubagent = parentThreadId !== null
86
+ || (isRecord(source) && Object.hasOwn(source, "subagent"))
87
+ || row.payload.thread_source === "subagent";
88
+ return { id, isSubagent, parentThreadId };
89
+ }
90
+ catch {
91
+ continue;
92
+ }
93
+ }
63
94
  }
64
- if (host.includes("claude_code")) {
65
- const candidate = env.CLAUDE_CODE_SESSION_ID?.trim() ?? "";
66
- return candidate
67
- ? verifiedSourceSession("claude-code", candidate, SOURCE_SESSION_MCP_METADATA_EVIDENCE)
68
- : null;
95
+ catch {
96
+ return null;
97
+ }
98
+ finally {
99
+ if (descriptor !== null)
100
+ fs.closeSync(descriptor);
69
101
  }
70
102
  return null;
71
103
  }
72
- function isRecord(value) {
73
- return typeof value === "object" && value !== null && !Array.isArray(value);
74
- }
75
104
  function readableDirectory(candidate) {
76
105
  try {
77
106
  return fs.statSync(candidate).isDirectory();
@@ -115,8 +144,105 @@ function walkJsonlFiles(roots) {
115
144
  }
116
145
  return files;
117
146
  }
118
- function recentFiles(roots, nowMs, maxAgeMs) {
119
- return walkJsonlFiles(roots)
147
+ /** Codex stores spawned-agent transcripts below `subagents`, so lineage discovery must include them. */
148
+ function walkCodexJsonlFiles(roots) {
149
+ const files = [];
150
+ const pending = roots.filter(readableDirectory);
151
+ while (pending.length) {
152
+ const directory = pending.pop();
153
+ let entries;
154
+ try {
155
+ entries = fs.readdirSync(directory, { withFileTypes: true });
156
+ }
157
+ catch {
158
+ continue;
159
+ }
160
+ for (const entry of entries) {
161
+ const target = path.join(directory, entry.name);
162
+ if (entry.isDirectory()) {
163
+ pending.push(target);
164
+ }
165
+ else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
166
+ files.push(target);
167
+ }
168
+ }
169
+ }
170
+ return files;
171
+ }
172
+ function codexFileIndexKey(roots) {
173
+ return [...roots].map((root) => path.resolve(root)).sort().join("\u0000");
174
+ }
175
+ function buildCodexFileIndex(roots) {
176
+ const filesById = new Map();
177
+ for (const file of walkCodexJsonlFiles(roots)) {
178
+ const filenameId = path.basename(file).match(UUID_IN_TEXT_RE)?.[0]?.toLowerCase();
179
+ if (filenameId && !filesById.has(filenameId))
180
+ filesById.set(filenameId, file);
181
+ }
182
+ return filesById;
183
+ }
184
+ function codexFileForSession(sessionId, roots, forceRefresh = false) {
185
+ const key = codexFileIndexKey(roots);
186
+ const now = Date.now();
187
+ let cached = codexFileIndexes.get(key);
188
+ if (forceRefresh || !cached || cached.expiresAtMs <= now) {
189
+ cached = {
190
+ expiresAtMs: now + CODEX_FILE_INDEX_TTL_MS,
191
+ filesById: buildCodexFileIndex(roots),
192
+ };
193
+ codexFileIndexes.set(key, cached);
194
+ }
195
+ const normalized = sessionId.toLowerCase();
196
+ return cached.filesById.get(normalized) ?? null;
197
+ }
198
+ function codexNodeForSession(sessionId, roots) {
199
+ let file = codexFileForSession(sessionId, roots);
200
+ if (!file)
201
+ file = codexFileForSession(sessionId, roots, true);
202
+ if (!file)
203
+ return null;
204
+ const node = codexLineageNode(file);
205
+ return node?.id === sessionId.toLowerCase() ? node : null;
206
+ }
207
+ function resolveCodexRootThread(agentThreadId, explicitParentThreadId, explicitSubagentMarker, roots) {
208
+ const localAgent = codexNodeForSession(agentThreadId, roots);
209
+ const isSubagent = explicitSubagentMarker || explicitParentThreadId !== null || localAgent?.isSubagent === true;
210
+ if (!isSubagent) {
211
+ return { isSubagent: false, rootThreadId: agentThreadId, parentThreadId: null, agentDepth: 0 };
212
+ }
213
+ const localParent = localAgent?.parentThreadId ?? null;
214
+ if (explicitParentThreadId && localParent && explicitParentThreadId !== localParent) {
215
+ return { isSubagent: true, rootThreadId: null, parentThreadId: explicitParentThreadId, agentDepth: 0 };
216
+ }
217
+ const firstParent = explicitParentThreadId ?? localParent;
218
+ if (!firstParent) {
219
+ return { isSubagent: true, rootThreadId: null, parentThreadId: null, agentDepth: 0 };
220
+ }
221
+ const visited = new Set([agentThreadId]);
222
+ let currentId = firstParent;
223
+ let depth = 1;
224
+ while (depth <= MAX_CODEX_AGENT_DEPTH) {
225
+ if (visited.has(currentId)) {
226
+ return { isSubagent: true, rootThreadId: null, parentThreadId: firstParent, agentDepth: depth };
227
+ }
228
+ visited.add(currentId);
229
+ const current = codexNodeForSession(currentId, roots);
230
+ if (!current) {
231
+ return { isSubagent: true, rootThreadId: null, parentThreadId: firstParent, agentDepth: depth };
232
+ }
233
+ if (!current.isSubagent) {
234
+ return { isSubagent: true, rootThreadId: current.id, parentThreadId: firstParent, agentDepth: depth };
235
+ }
236
+ if (!current.parentThreadId) {
237
+ return { isSubagent: true, rootThreadId: null, parentThreadId: firstParent, agentDepth: depth };
238
+ }
239
+ currentId = current.parentThreadId;
240
+ depth += 1;
241
+ }
242
+ return { isSubagent: true, rootThreadId: null, parentThreadId: firstParent, agentDepth: depth };
243
+ }
244
+ function recentFiles(roots, nowMs, maxAgeMs, includeCodexSubagents = false) {
245
+ return (includeCodexSubagents ? walkCodexJsonlFiles(roots) : walkJsonlFiles(roots))
120
246
  .map((file) => {
121
247
  try {
122
248
  return { file, mtimeMs: fs.statSync(file).mtimeMs };
@@ -151,41 +277,11 @@ function tailContainsBinding(file, bindingToken) {
151
277
  fs.closeSync(descriptor);
152
278
  }
153
279
  }
154
- function codexSessionId(file) {
155
- let descriptor = null;
156
- try {
157
- descriptor = fs.openSync(file, "r");
158
- const stat = fs.fstatSync(descriptor);
159
- const bytes = Math.min(stat.size, 128 * 1024);
160
- const buffer = Buffer.allocUnsafe(bytes);
161
- fs.readSync(descriptor, buffer, 0, bytes, 0);
162
- for (const line of buffer.toString("utf8").split(/\r?\n/)) {
163
- if (!line.includes("session_meta"))
164
- continue;
165
- try {
166
- const row = JSON.parse(line);
167
- if (!isRecord(row) || row.type !== "session_meta" || !isRecord(row.payload))
168
- continue;
169
- if (typeof row.payload.parent_thread_id === "string" && row.payload.parent_thread_id)
170
- return null;
171
- const raw = typeof row.payload.id === "string"
172
- ? row.payload.id
173
- : typeof row.payload.session_id === "string"
174
- ? row.payload.session_id
175
- : "";
176
- return UUID_RE.test(raw) ? raw.toLowerCase() : null;
177
- }
178
- catch {
179
- continue;
180
- }
181
- }
182
- }
183
- catch {
184
- return null;
185
- }
186
- finally {
187
- if (descriptor !== null)
188
- fs.closeSync(descriptor);
280
+ function codexSessionId(file, roots) {
281
+ const node = codexLineageNode(file);
282
+ if (node) {
283
+ const resolution = resolveCodexRootThread(node.id, node.parentThreadId, node.isSubagent, roots);
284
+ return resolution.rootThreadId;
189
285
  }
190
286
  return path.basename(file).match(UUID_IN_TEXT_RE)?.[0]?.toLowerCase() ?? null;
191
287
  }
@@ -208,6 +304,88 @@ function enabledProviders(hostPlatform) {
208
304
  }
209
305
  return ["codex", "claude-code", "cowork"];
210
306
  }
307
+ /**
308
+ * Resolve host-owned request metadata into EchoMem's root human-thread context. A Codex subagent
309
+ * gets its own model/tool transcript, but its durable memory context remains the originating root.
310
+ * Child lineage must be corroborated by local host JSONL; failures stay explicitly unbound.
311
+ */
312
+ export function resolveSourceSessionRequestContext(metadata, options = {}) {
313
+ const host = options.hostPlatform?.toLowerCase().replace(/[^a-z0-9]+/g, "_") ?? "";
314
+ const env = options.env ?? process.env;
315
+ if (host.includes("codex")) {
316
+ const root = isRecord(metadata) ? metadata : {};
317
+ const turn = isRecord(root["x-codex-turn-metadata"])
318
+ ? root["x-codex-turn-metadata"]
319
+ : {};
320
+ const agentThreadId = normalizedUuid(turn.thread_id)
321
+ ?? normalizedUuid(turn.session_id)
322
+ ?? normalizedUuid(root.threadId);
323
+ const parentThreadId = normalizedUuid(root["x-codex-parent-thread-id"])
324
+ ?? normalizedUuid(turn.parent_thread_id)
325
+ ?? normalizedUuid(turn.parentThreadId)
326
+ ?? sourceParentThreadId(turn.source);
327
+ const explicitSubagentMarker = parentThreadId !== null
328
+ || metadataMarksSubagent(root["x-openai-subagent"])
329
+ || turn.thread_source === "subagent"
330
+ || metadataMarksSubagent(turn.subagent_kind)
331
+ || (isRecord(turn.source) && Object.hasOwn(turn.source, "subagent"));
332
+ if (!agentThreadId) {
333
+ return {
334
+ sourceSession: null,
335
+ isSubagent: explicitSubagentMarker,
336
+ lineageStatus: explicitSubagentMarker ? "unresolved_subagent" : "unbound",
337
+ parentThreadId: parentThreadId ?? undefined,
338
+ };
339
+ }
340
+ const codexRoots = options.roots?.codex
341
+ ?? resolveCodexSessionRoots({ env }).map((entry) => entry.path);
342
+ const lineage = resolveCodexRootThread(agentThreadId, parentThreadId, explicitSubagentMarker, codexRoots);
343
+ if (lineage.isSubagent && !lineage.rootThreadId) {
344
+ return {
345
+ sourceSession: null,
346
+ isSubagent: true,
347
+ lineageStatus: "unresolved_subagent",
348
+ agentThreadId,
349
+ parentThreadId: lineage.parentThreadId ?? undefined,
350
+ agentDepth: lineage.agentDepth,
351
+ };
352
+ }
353
+ const rootThreadId = lineage.rootThreadId ?? agentThreadId;
354
+ return {
355
+ sourceSession: verifiedSourceSession("codex", rootThreadId, SOURCE_SESSION_MCP_METADATA_EVIDENCE),
356
+ isSubagent: lineage.isSubagent,
357
+ lineageStatus: lineage.isSubagent ? "resolved_subagent" : "top_level",
358
+ agentThreadId,
359
+ parentThreadId: lineage.parentThreadId ?? undefined,
360
+ rootThreadId,
361
+ agentDepth: lineage.agentDepth,
362
+ };
363
+ }
364
+ if (host.includes("claude_desktop") || host === "claude" || host.includes("cowork")) {
365
+ const candidate = env.CLAUDE_CODE_HOST_SESSION_ID?.trim() ?? "";
366
+ return {
367
+ sourceSession: candidate
368
+ ? verifiedSourceSession("cowork", candidate, SOURCE_SESSION_MCP_METADATA_EVIDENCE)
369
+ : null,
370
+ isSubagent: false,
371
+ lineageStatus: candidate ? "not_applicable" : "unbound",
372
+ };
373
+ }
374
+ if (host.includes("claude_code")) {
375
+ const candidate = env.CLAUDE_CODE_SESSION_ID?.trim() ?? "";
376
+ return {
377
+ sourceSession: candidate
378
+ ? verifiedSourceSession("claude-code", candidate, SOURCE_SESSION_MCP_METADATA_EVIDENCE)
379
+ : null,
380
+ isSubagent: false,
381
+ lineageStatus: candidate ? "not_applicable" : "unbound",
382
+ };
383
+ }
384
+ return { sourceSession: null, isSubagent: false, lineageStatus: "unbound" };
385
+ }
386
+ export function resolveSourceSessionFromMcpContext(metadata, options = {}) {
387
+ return resolveSourceSessionRequestContext(metadata, options).sourceSession;
388
+ }
211
389
  export function resolveSourceSessionFromBindingToken(bindingToken, options = {}) {
212
390
  const normalizedToken = bindingToken.trim().toLowerCase();
213
391
  if (!UUID_RE.test(normalizedToken)) {
@@ -231,11 +409,11 @@ export function resolveSourceSessionFromBindingToken(bindingToken, options = {})
231
409
  : provider === "claude-code"
232
410
  ? roots.claudeCode
233
411
  : roots.cowork;
234
- for (const file of recentFiles(providerRoots, nowMs, maxAgeMs)) {
412
+ for (const file of recentFiles(providerRoots, nowMs, maxAgeMs, provider === "codex")) {
235
413
  if (!tailContainsBinding(file, normalizedToken))
236
414
  continue;
237
415
  const providerSessionId = provider === "codex"
238
- ? codexSessionId(file)
416
+ ? codexSessionId(file, roots.codex)
239
417
  : provider === "claude-code"
240
418
  ? claudeCodeSessionId(file)
241
419
  : coworkSessionId(file);
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { MCP_DESKTOP_MANAGED, MCP_VAULT_UNLOCK_INSTRUCTION, MEMORY_CITATION_INSTRUCTION, SAVED_MEMORY_RECEIPT_INSTRUCTION, withMcpVersion, } from "./package-metadata.js";
3
3
  export const canonicalToolNames = {
4
4
  bindSourceSession: "bind_source_session",
5
+ linkWorkspaceTicketSession: "link_workspace_ticket_session",
5
6
  search: "search_memories",
6
7
  save: "save_conversation",
7
8
  timeRange: "get_memories_by_time_range",
@@ -59,6 +60,7 @@ const READ_ONLY_TOOL_NAMES = new Set([
59
60
  ]);
60
61
  const IDEMPOTENT_WRITE_TOOL_NAMES = new Set([
61
62
  canonicalToolNames.bindSourceSession,
63
+ canonicalToolNames.linkWorkspaceTicketSession,
62
64
  canonicalToolNames.recordCitations,
63
65
  canonicalToolNames.setGroupSessionSharing,
64
66
  canonicalToolNames.updateGroupProfile,
@@ -78,6 +80,7 @@ const REQUIRES_USER_INTERACTION_TOOL_NAMES = new Set([
78
80
  ]);
79
81
  const ALWAYS_LOAD_TOOL_NAMES = new Set([
80
82
  canonicalToolNames.bindSourceSession,
83
+ canonicalToolNames.linkWorkspaceTicketSession,
81
84
  canonicalToolNames.search,
82
85
  canonicalToolNames.save,
83
86
  ]);
@@ -226,6 +229,11 @@ export const saveConversationSchema = z.object({
226
229
  export const bindSourceSessionSchema = z.object({
227
230
  bindingToken: z.string().uuid(),
228
231
  });
232
+ export const linkWorkspaceTicketSessionSchema = z.object({
233
+ ...triggerMetadataSchema,
234
+ ticketId: z.string().uuid(),
235
+ workspaceId: z.string().uuid().optional(),
236
+ });
229
237
  const dateBoundarySchema = z.string().trim().min(1).refine((value) => Number.isFinite(Date.parse(value)), "Expected an ISO-8601 date or date-time string");
230
238
  export const timeRangeSchema = z.object({
231
239
  ...triggerMetadataSchema,
@@ -467,6 +475,32 @@ export function listToolSpecs(opts = {}) {
467
475
  required: ["bindingToken"],
468
476
  },
469
477
  },
478
+ {
479
+ name: canonicalToolNames.linkWorkspaceTicketSession,
480
+ 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."),
482
+ inputSchema: {
483
+ type: "object",
484
+ properties: {
485
+ ticketId: {
486
+ type: "string",
487
+ format: "uuid",
488
+ description: "The Echo workspace ticket UUID from the user's message or structured launch marker.",
489
+ },
490
+ workspaceId: {
491
+ type: "string",
492
+ format: "uuid",
493
+ description: "Optional Echo workspace UUID when it is present in the structured launch marker.",
494
+ },
495
+ triggerMessage: {
496
+ type: "string",
497
+ description: "Optional user message that expressed the ticket-linking intent.",
498
+ },
499
+ triggerMessageRole: { type: "string", default: "user" },
500
+ },
501
+ required: ["ticketId"],
502
+ },
503
+ },
470
504
  {
471
505
  name: canonicalToolNames.search,
472
506
  title: "Search your memories by topic",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.49",
3
+ "version": "1.4.51",
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",
@@ -31,7 +31,9 @@
31
31
  "test:ui": "npm run build && node test/setup-ui.test.mjs",
32
32
  "test:onboarding-resilience": "npm run build && node test/onboarding-resilience.test.mjs",
33
33
  "test:billing-ui": "npm run build && node test/setup-ui.test.mjs billing",
34
- "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/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",
34
+ "test:config-safety": "npm run build && node test/config-safety.test.mjs",
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",
35
37
  "prepack": "npm run build"
36
38
  },
37
39
  "dependencies": {