@mnemonik/shared 7.97.1 → 7.98.0

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.
Files changed (130) hide show
  1. package/dist/ambientPointer.d.ts.map +1 -1
  2. package/dist/ambientPointer.js +2 -1
  3. package/dist/ambientPointer.js.map +1 -1
  4. package/dist/apiOrigin.d.ts +3 -0
  5. package/dist/apiOrigin.d.ts.map +1 -0
  6. package/dist/apiOrigin.js +5 -0
  7. package/dist/apiOrigin.js.map +1 -0
  8. package/dist/ast/astChunker.d.ts +12 -12
  9. package/dist/ast/astChunker.js +32 -32
  10. package/dist/ast/grammars.d.ts +9 -9
  11. package/dist/ast/grammars.js +15 -15
  12. package/dist/codeScanner.d.ts +56 -43
  13. package/dist/codeScanner.d.ts.map +1 -1
  14. package/dist/codeScanner.js +262 -143
  15. package/dist/codeScanner.js.map +1 -1
  16. package/dist/contextBudget.js +2 -2
  17. package/dist/contextBudget.js.map +1 -1
  18. package/dist/gitEditDetector.d.ts +6 -6
  19. package/dist/gitEditDetector.js +7 -7
  20. package/dist/hookRuntime.d.ts +35 -2
  21. package/dist/hookRuntime.d.ts.map +1 -1
  22. package/dist/hookRuntime.js +127 -33
  23. package/dist/hookRuntime.js.map +1 -1
  24. package/dist/hookTimeouts.d.ts +6 -6
  25. package/dist/hookTimeouts.js +6 -6
  26. package/dist/hostAdapter.d.ts +135 -0
  27. package/dist/hostAdapter.d.ts.map +1 -0
  28. package/dist/hostAdapter.js +323 -0
  29. package/dist/hostAdapter.js.map +1 -0
  30. package/dist/hostBinary.d.ts +29 -0
  31. package/dist/hostBinary.d.ts.map +1 -0
  32. package/dist/hostBinary.js +179 -0
  33. package/dist/hostBinary.js.map +1 -0
  34. package/dist/index.d.ts +11 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +10 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/instructions.d.ts +26 -26
  39. package/dist/instructions.js +26 -26
  40. package/dist/jitDeliveryAck.d.ts.map +1 -1
  41. package/dist/jitDeliveryAck.js +2 -0
  42. package/dist/jitDeliveryAck.js.map +1 -1
  43. package/dist/projectIdentityFile.d.ts +25 -0
  44. package/dist/projectIdentityFile.d.ts.map +1 -0
  45. package/dist/projectIdentityFile.js +87 -0
  46. package/dist/projectIdentityFile.js.map +1 -0
  47. package/dist/projectSetupHandoff.d.ts +24 -0
  48. package/dist/projectSetupHandoff.d.ts.map +1 -0
  49. package/dist/projectSetupHandoff.js +249 -0
  50. package/dist/projectSetupHandoff.js.map +1 -0
  51. package/dist/protectedPaths.d.ts +9 -0
  52. package/dist/protectedPaths.d.ts.map +1 -0
  53. package/dist/protectedPaths.js +66 -0
  54. package/dist/protectedPaths.js.map +1 -0
  55. package/dist/readiness.d.ts +143 -0
  56. package/dist/readiness.d.ts.map +1 -0
  57. package/dist/readiness.js +304 -0
  58. package/dist/readiness.js.map +1 -0
  59. package/dist/repositoryFingerprint.d.ts +50 -0
  60. package/dist/repositoryFingerprint.d.ts.map +1 -0
  61. package/dist/repositoryFingerprint.js +160 -0
  62. package/dist/repositoryFingerprint.js.map +1 -0
  63. package/dist/repositoryRoot.d.ts +56 -0
  64. package/dist/repositoryRoot.d.ts.map +1 -0
  65. package/dist/repositoryRoot.js +226 -0
  66. package/dist/repositoryRoot.js.map +1 -0
  67. package/dist/runtimeReader.d.ts +102 -0
  68. package/dist/runtimeReader.d.ts.map +1 -0
  69. package/dist/runtimeReader.js +408 -0
  70. package/dist/runtimeReader.js.map +1 -0
  71. package/dist/runtimeSigners.d.ts +37 -0
  72. package/dist/runtimeSigners.d.ts.map +1 -0
  73. package/dist/runtimeSigners.js +507 -0
  74. package/dist/runtimeSigners.js.map +1 -0
  75. package/dist/scannerSupervisor.d.ts +35 -0
  76. package/dist/scannerSupervisor.d.ts.map +1 -0
  77. package/dist/scannerSupervisor.js +2 -0
  78. package/dist/scannerSupervisor.js.map +1 -0
  79. package/dist/secretPatterns.d.ts +9 -9
  80. package/dist/secretPatterns.d.ts.map +1 -1
  81. package/dist/secretPatterns.js +27 -25
  82. package/dist/secretPatterns.js.map +1 -1
  83. package/dist/testing/hostAdapterConformance.d.ts +17 -0
  84. package/dist/testing/hostAdapterConformance.d.ts.map +1 -0
  85. package/dist/testing/hostAdapterConformance.js +477 -0
  86. package/dist/testing/hostAdapterConformance.js.map +1 -0
  87. package/dist/usageGuide.d.ts +1 -1
  88. package/dist/usageGuide.d.ts.map +1 -1
  89. package/dist/usageGuide.js +4 -4
  90. package/package.json +7 -2
  91. package/queries/bash.tags.scm +1 -1
  92. package/queries/groovy.tags.scm +1 -1
  93. package/scripts/vendor-grammars.ts +13 -13
  94. package/src/ambientPointer.ts +2 -1
  95. package/src/apiOrigin.ts +4 -0
  96. package/src/ast/astChunker.ts +39 -39
  97. package/src/ast/grammars.ts +16 -16
  98. package/src/codeScanner.ts +274 -149
  99. package/src/contextBudget.ts +2 -2
  100. package/src/gitEditDetector.ts +7 -7
  101. package/src/hookRuntime.ts +162 -34
  102. package/src/hookTimeouts.ts +6 -6
  103. package/src/hostAdapter.ts +468 -0
  104. package/src/hostBinary.ts +205 -0
  105. package/src/index.ts +20 -0
  106. package/src/instructions.ts +26 -26
  107. package/src/jitDeliveryAck.ts +2 -0
  108. package/src/projectIdentityFile.ts +100 -0
  109. package/src/projectSetupHandoff.ts +275 -0
  110. package/src/protectedPaths.ts +88 -0
  111. package/src/readiness.ts +516 -0
  112. package/src/repositoryFingerprint.ts +199 -0
  113. package/src/repositoryRoot.ts +261 -0
  114. package/src/runtimeReader.ts +495 -0
  115. package/src/runtimeSigners.ts +577 -0
  116. package/src/scannerSupervisor.ts +26 -0
  117. package/src/secretPatterns.ts +27 -25
  118. package/src/testing/hostAdapterConformance.ts +538 -0
  119. package/src/usageGuide.ts +4 -4
  120. package/test-fixtures/identity/runner.d.mts +12 -0
  121. package/test-fixtures/identity/runner.mjs +282 -0
  122. package/tests/fixtures/repository-fingerprint-v1.golden.json +834 -0
  123. package/tests/projectIdentity.test.ts +56 -0
  124. package/tests/readiness.test.ts +158 -0
  125. package/tests/repositoryFingerprint.test.ts +26 -0
  126. package/tsconfig.test.json +9 -0
  127. package/wasm/bash.tags.scm +1 -1
  128. package/wasm/groovy.tags.scm +1 -1
  129. package/wasm/javascript.tags.scm +2 -2
  130. package/wasm/solidity.tags.scm +1 -1
@@ -37,10 +37,10 @@ export interface BudgetedContextFit {
37
37
  }
38
38
 
39
39
  const JOINER = '\n\n';
40
- const NOT_REACHED_PREFIX = '… not reached: ';
40
+ const NOT_REACHED_PREFIX = '... not reached: ';
41
41
 
42
42
  function truncatedMemoryMarker(memoryId: string): string {
43
- return `… [truncated; memory_get ${memoryId} for the rest]`;
43
+ return `... [truncated; memory_get ${memoryId} for the rest]`;
44
44
  }
45
45
 
46
46
  function itemMemoryIds(item: ContextBudgetItem): readonly string[] {
@@ -2,7 +2,7 @@
2
2
  * Shell-write classification via filesystem observation, not command parsing.
3
3
  *
4
4
  * Permissive/auto-approve modes steer agents toward the shell for file work on
5
- * every host, and a shell payload carries a command string, not a file path —
5
+ * every host, and a shell payload carries a command string, not a file path -
6
6
  * so post-shell events reached the server with no edit to report and the
7
7
  * session's filesEdited/ideEditCount stayed empty, silencing every edit-gated
8
8
  * signal (checkpoint pressure above all). Parsing the command text for written
@@ -14,7 +14,7 @@
14
14
  * at session start, and after each shell call diff the current set against the
15
15
  * snapshot. Paths that are newly dirty were written between the two
16
16
  * observations. The caller reports those paths, then adds only confirmed paths
17
- * to the snapshot. Only additions count — a commit or stash SHRINKS the set,
17
+ * to the snapshot. Only additions count - a commit or stash SHRINKS the set,
18
18
  * and saving work is not new work. Edit tool writes are unioned into the
19
19
  * snapshot by each host's edit handler so they are never re-credited to the
20
20
  * next shell call.
@@ -22,7 +22,7 @@
22
22
  * Host-agnostic by construction: the caller owns session identity and passes
23
23
  * the absolute path of its own private snapshot file. An empty `snapshotFile`
24
24
  * is the caller's "no usable session identity" signal and every entry point
25
- * no-ops on it — a synthesized or fallback id must never own a snapshot.
25
+ * no-ops on it - a synthesized or fallback id must never own a snapshot.
26
26
  * Because the diff rolls the snapshot forward, it is idempotent: a host with
27
27
  * no post-shell event may call it from several carrier events and each new
28
28
  * write is still reported exactly once.
@@ -33,7 +33,7 @@
33
33
  * - Any working-tree change between two observations is credited to the shell
34
34
  * call, including background processes. For "does this session have unsaved
35
35
  * work" that is the right signal.
36
- * - Only the repo containing cwd is observed. Not a git repo → report nothing
36
+ * - Only the repo containing cwd is observed. Not a git repo -> report nothing
37
37
  * rather than guess.
38
38
  */
39
39
 
@@ -94,7 +94,7 @@ export function listGitDirtyPaths(cwd: string): { root: string; paths: string[]
94
94
  const entry = tokens[i];
95
95
  if (!entry || entry.length < 4) continue;
96
96
  // "XY <path>"; a rename/copy entry is followed by the ORIGINAL path as its
97
- // own NUL token — consume it so it is never misread as a status entry.
97
+ // own NUL token - consume it so it is never misread as a status entry.
98
98
  const statusCode = entry.slice(0, 2);
99
99
  paths.push(join(root, entry.slice(3)));
100
100
  if (statusCode.startsWith('R') || statusCode.startsWith('C')) i++;
@@ -139,7 +139,7 @@ export function captureGitDirtyBaseline(snapshotFile: string, cwd: string): void
139
139
  * After a shell call: paths newly dirty since the last observation, capped.
140
140
  * Leaves additions pending until the caller confirms them with
141
141
  * addPathsToGitDirtySnapshot. Without a usable baseline it establishes one and
142
- * reports nothing — degrading toward silence, never toward a false edit.
142
+ * reports nothing - degrading toward silence, never toward a false edit.
143
143
  */
144
144
  export function diffGitDirtySnapshot(snapshotFile: string, cwd: string): string[] {
145
145
  if (!snapshotFile) return [];
@@ -162,7 +162,7 @@ export function diffGitDirtySnapshot(snapshotFile: string, cwd: string): string[
162
162
 
163
163
  /**
164
164
  * After an edit tool: fold tool-reported edits into the snapshot so the next
165
- * shell diff does not re-credit them. Only when a baseline exists — seeding a
165
+ * shell diff does not re-credit them. Only when a baseline exists - seeding a
166
166
  * partial snapshot would make later diffs report pre-existing dirt.
167
167
  */
168
168
  export function addPathsToGitDirtySnapshot(snapshotFile: string, paths: string[]): void {
@@ -1,48 +1,32 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { dirname, isAbsolute, join, resolve } from 'node:path';
1
+ import { protectWindowsDirectory, type Execute } from './runtimeSigners.js';
3
2
  import { stderr } from 'node:process';
3
+ import { resolveProjectIdentity, type ProjectIdentityResolution } from './repositoryRoot.js';
4
4
 
5
5
  export interface ProjectIdentity {
6
- projectId?: string;
6
+ projectId: string;
7
7
  projectName?: string;
8
8
  projectRoot: string;
9
9
  }
10
10
 
11
- const identityCache = new Map<string, ProjectIdentity | null>();
12
-
13
11
  export async function findProjectIdentity(
14
12
  startCwd: string,
15
- opts: { maxDepth?: number } = {}
13
+ options: { allowNestedInherit?: boolean; maxDepth?: number } = {}
16
14
  ): Promise<ProjectIdentity | null> {
17
15
  if (!startCwd) return null;
18
- const cached = identityCache.get(startCwd);
19
- if (cached !== undefined) return cached;
16
+ const result = await findProjectIdentityDetailed(startCwd, options);
17
+ if (result.kind !== 'ok') return null;
18
+ return {
19
+ projectId: result.identity.projectId,
20
+ projectName: result.identity.projectName,
21
+ projectRoot: result.root,
22
+ };
23
+ }
20
24
 
21
- let directory = isAbsolute(startCwd) ? startCwd : resolve(startCwd);
22
- const maxDepth = opts.maxDepth ?? 10;
23
- for (let depth = 0; depth <= maxDepth; depth += 1) {
24
- try {
25
- const parsed = JSON.parse(
26
- await readFile(join(directory, '.mnemonik.json'), 'utf8')
27
- ) as Record<string, unknown>;
28
- if (typeof parsed.projectId === 'string' && parsed.projectId.length > 0) {
29
- const identity: ProjectIdentity = {
30
- projectId: parsed.projectId,
31
- projectName: typeof parsed.projectName === 'string' ? parsed.projectName : undefined,
32
- projectRoot: directory,
33
- };
34
- identityCache.set(startCwd, identity);
35
- return identity;
36
- }
37
- } catch {
38
- // Missing, unreadable, or malformed: keep walking upward.
39
- }
40
- const parent = dirname(directory);
41
- if (parent === directory) break;
42
- directory = parent;
43
- }
44
- identityCache.set(startCwd, null);
45
- return null;
25
+ export async function findProjectIdentityDetailed(
26
+ startCwd: string,
27
+ options: { allowNestedInherit?: boolean; maxDepth?: number } = {}
28
+ ): Promise<ProjectIdentityResolution> {
29
+ return resolveProjectIdentity(startCwd, options);
46
30
  }
47
31
 
48
32
  let mismatchEmitted = false;
@@ -52,7 +36,7 @@ export async function parseHookResponse<T>(response: Response): Promise<T> {
52
36
  const result = body as { ok?: boolean; reason?: string };
53
37
  if (!mismatchEmitted && result?.ok === false && result.reason === 'identity_mismatch') {
54
38
  stderr.write(
55
- 'mnemonik-hook: project identity mismatch — .mnemonik.json points to one project, ' +
39
+ 'mnemonik-hook: project identity mismatch - .mnemonik.json points to one project, ' +
56
40
  'but this directory is registered to a different one on the server. ' +
57
41
  'Fix .mnemonik.json (delete it and let session_bootstrap rewrite it) or cd to the correct project root.\n'
58
42
  );
@@ -60,3 +44,147 @@ export async function parseHookResponse<T>(response: Response): Promise<T> {
60
44
  }
61
45
  return body;
62
46
  }
47
+
48
+ /** Native host correlation only; never accept checkpoint/model fields here. */
49
+ export function validHookSessionId(value: unknown): value is string {
50
+ return (
51
+ typeof value === 'string' &&
52
+ /^[a-zA-Z0-9@._+:-]{1,255}$/.test(value) &&
53
+ !['cursor-unknown-session', 'codex-unknown-session'].includes(value)
54
+ );
55
+ }
56
+
57
+ export interface HookBindingInput {
58
+ host: 'claude_code' | 'cursor' | 'grok';
59
+ hostSessionId: string;
60
+ cwd: string;
61
+ server: string;
62
+ stateFile: string;
63
+ }
64
+
65
+ /** Bounded JSON transport; never log credentials, request/response bodies or URLs. */
66
+ export async function postHookBoundJson(server: string, path: string, token: string, body: object) {
67
+ const response = await fetch(`${server.replace(/\/$/, '')}${path}`, {
68
+ method: 'POST',
69
+ headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
70
+ body: JSON.stringify(body),
71
+ signal: AbortSignal.timeout(2_000),
72
+ });
73
+ return { status: response.status, body: (await response.json()) as unknown };
74
+ }
75
+
76
+ /** Shared context/cache logic. Credentials remain owned by each host package's
77
+ * adapter callback so shared acquires no runtime credential dependency. */
78
+ export async function bindHookContext(
79
+ input: HookBindingInput,
80
+ familyId: string,
81
+ hmac: (root: string) => Promise<{ version: 1; hmac: string }>,
82
+ post: (body: object) => Promise<number | string>,
83
+ options: { platform?: NodeJS.Platform; run?: Execute } = {}
84
+ ): Promise<void> {
85
+ const { execFile } = await import('node:child_process');
86
+ const { constants } = await import('node:fs');
87
+ const { lstat, mkdir, open, realpath } = await import('node:fs/promises');
88
+ const { dirname } = await import('node:path');
89
+ // Lazy: legacy durable runtimes may not carry the fingerprint module.
90
+ const { selectRemote } = await import('./repositoryFingerprint.js');
91
+ const identity = await findProjectIdentityDetailed(await realpath(input.cwd));
92
+ if (identity.kind === 'git_unavailable') throw new Error('context unavailable');
93
+ const root = await realpath(identity.root);
94
+ const binding = await hmac(root);
95
+ const gitEnv: NodeJS.ProcessEnv = { LC_ALL: 'C' };
96
+ for (const key of ['PATH', 'HOME', 'LANG', 'SYSTEMROOT', 'TEMP', 'TMP', 'USERPROFILE'])
97
+ if (process.env[key] !== undefined) gitEnv[key] = process.env[key];
98
+ const git = (args: string[]): Promise<string[]> =>
99
+ new Promise((resolve) => {
100
+ execFile(
101
+ 'git',
102
+ args,
103
+ { cwd: root, env: gitEnv, timeout: 2_000, encoding: 'utf8' },
104
+ (error, stdout) => resolve(error ? [] : stdout.trim().split(/\r?\n/).filter(Boolean))
105
+ );
106
+ });
107
+ const remotes = identity.repository.kind === 'git' ? await git(['remote']) : [];
108
+ const selection = selectRemote(
109
+ await Promise.all(
110
+ remotes.map(async (name) => ({
111
+ name,
112
+ fetchUrls: await git(['remote', 'get-url', '--all', name]),
113
+ pushUrls: await git(['remote', 'get-url', '--push', '--all', name]),
114
+ }))
115
+ )
116
+ );
117
+ const body = {
118
+ host: input.host,
119
+ hostSessionId: input.hostSessionId,
120
+ deviceRootContext: {
121
+ algorithmVersion: binding.version,
122
+ hash: Buffer.from(binding.hmac, 'base64url').toString('hex'),
123
+ },
124
+ repositoryFingerprint:
125
+ selection.status === 'fingerprint'
126
+ ? {
127
+ algorithmVersion: selection.fingerprint.algorithmVersion,
128
+ hash: selection.fingerprint.hash,
129
+ }
130
+ : null,
131
+ // Automatic hooks never select a non-git root for the user.
132
+ rootKind: identity.repository.kind === 'git' ? 'git' : 'ineligible',
133
+ identityState:
134
+ identity.kind === 'ok' ? 'valid' : identity.kind === 'absent' ? 'absent' : 'invalid',
135
+ ...(identity.kind === 'ok' ? { projectId: identity.identity.projectId } : {}),
136
+ };
137
+ const key = JSON.stringify([input.server, familyId, body]);
138
+ const directory = dirname(input.stateFile);
139
+ for (const path of [dirname(directory), directory]) {
140
+ const created = await mkdir(path, { recursive: true, mode: 0o700 });
141
+ const stat = await lstat(path);
142
+ if (
143
+ !stat.isDirectory() ||
144
+ stat.isSymbolicLink() ||
145
+ ((options.platform ?? process.platform) !== 'win32' &&
146
+ ((process.getuid && stat.uid !== process.getuid()) || stat.mode & 0o077))
147
+ )
148
+ throw new Error('cache unavailable');
149
+ if ((options.platform ?? process.platform) === 'win32') {
150
+ await protectWindowsDirectory(path, created !== undefined, options.run, directory);
151
+ }
152
+ }
153
+ const file = await open(
154
+ input.stateFile,
155
+ constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW,
156
+ 0o600
157
+ );
158
+ try {
159
+ let cache:
160
+ { key: string; expiresAt: number; retryKey?: string; canonicalRoot?: string } | undefined;
161
+ try {
162
+ cache = JSON.parse(await file.readFile('utf8'));
163
+ } catch {
164
+ /* First event. */
165
+ }
166
+ const now = Date.now();
167
+ if (cache && cache.expiresAt > now && (cache.key === key || cache.retryKey === key)) return;
168
+ const status = await post(body);
169
+ if (status === 200) cache = { key, expiresAt: Date.now() + 120_000, canonicalRoot: root };
170
+ else {
171
+ stderr.write(`mnemonik-hook: bound-context post failed (${status})\n`);
172
+ if (status !== 409) return;
173
+ // A changed root/identity cannot replace a live server binding. Retry at
174
+ // its original expiry; without a local receipt, wait one conservative TTL.
175
+ cache = {
176
+ key: cache?.key ?? '',
177
+ expiresAt: cache && cache.expiresAt > now ? cache.expiresAt : now + 120_000,
178
+ retryKey: key,
179
+ };
180
+ }
181
+ await file.truncate(0);
182
+ await file.write(JSON.stringify(cache), 0, 'utf8');
183
+ } finally {
184
+ await file.close();
185
+ }
186
+ }
187
+
188
+ export * from './runtimeReader.js';
189
+ export * from './runtimeSigners.js';
190
+ export * from './projectSetupHandoff.js';
@@ -6,11 +6,11 @@
6
6
  * `@mnemonik/codex-hooks`, `@mnemonik/cursor-hooks`). Before the
7
7
  * 2026-05-16 audit Finding #7 cross-cutting cleanup, each package
8
8
  * declared its own copy of these constants and a near-identical
9
- * `withTimeout` helper — coordinating a budget change required three
9
+ * `withTimeout` helper - coordinating a budget change required three
10
10
  * synchronised edits with no enforcement that the values matched.
11
11
  *
12
12
  * Surface is intentionally minimal: small constants + a single helper
13
- * function. No fetch wrappers here — request shaping stays per-package
13
+ * function. No fetch wrappers here - request shaping stays per-package
14
14
  * because each host expresses its hook payloads differently.
15
15
  */
16
16
 
@@ -20,8 +20,8 @@
20
20
  * The bootstrap digest is delivered over `injections`, so it rides this 2s
21
21
  * budget. That is intentional and sufficient: on a warm cache
22
22
  * (HooksDispatcher.buildBootstrapDigestPayload) delivery is a Redis GET plus
23
- * render; on a cold cache the delivery path awaits buildColdFloorDigest — a
24
- * parallel Promise.all of cheap DB reads raced under a 1500ms sub-budget —
23
+ * render; on a cold cache the delivery path awaits buildColdFloorDigest - a
24
+ * parallel Promise.all of cheap DB reads raced under a 1500ms sub-budget -
25
25
  * and may return an enriched floor digest. The static `session_bootstrap`
26
26
  * directive is the fallback only when the floor is entirely empty (or the
27
27
  * floor fetch times out). A full synthesis build is fired in the background
@@ -40,7 +40,7 @@ export const POST_TOOL_TIMEOUT_MS = 1500;
40
40
 
41
41
  /**
42
42
  * beforeMCPExecution gate budget. This is the ONLY synchronous, user-facing
43
- * gate — the user waits on it before every MCP tool call — so it fails open
43
+ * gate - the user waits on it before every MCP tool call - so it fails open
44
44
  * faster than the background critical fetch (FETCH_TIMEOUT_MS = 2000). A
45
45
  * precheck slower than 1s is not worth blocking the tool call for; on a
46
46
  * degraded server we drop the gate rather than stall the user.
@@ -50,7 +50,7 @@ export const MCP_PRECHECK_TIMEOUT_MS = 1000;
50
50
  /**
51
51
  * Spawn an `AbortController` tied to a timeout. Returns the signal plus a
52
52
  * `cleanup` function the caller MUST invoke (in `finally`) to clear the
53
- * timer when the request finishes naturally — otherwise the timer leaks
53
+ * timer when the request finishes naturally - otherwise the timer leaks
54
54
  * for the timeout duration.
55
55
  *
56
56
  * Identical signature to the inlined `withTimeout` that each hook package