@j0hanz/filesystem-mcp 1.1.2 → 1.2.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 (62) hide show
  1. package/README.md +514 -188
  2. package/dist/cli.js +29 -12
  3. package/dist/completions.js +50 -24
  4. package/dist/config.d.ts +3 -2
  5. package/dist/config.js +1 -1
  6. package/dist/index.js +14 -12
  7. package/dist/instructions.md +109 -97
  8. package/dist/lib/constants.js +25 -14
  9. package/dist/lib/errors.js +11 -6
  10. package/dist/lib/file-operations/common.d.ts +4 -0
  11. package/dist/lib/file-operations/common.js +9 -0
  12. package/dist/lib/file-operations/file-info.js +22 -10
  13. package/dist/lib/file-operations/gitignore.js +14 -11
  14. package/dist/lib/file-operations/glob-engine.d.ts +1 -0
  15. package/dist/lib/file-operations/glob-engine.js +46 -33
  16. package/dist/lib/file-operations/list-directory.js +31 -35
  17. package/dist/lib/file-operations/read-multiple-files.js +70 -62
  18. package/dist/lib/file-operations/search-content.js +83 -64
  19. package/dist/lib/file-operations/search-files.js +32 -30
  20. package/dist/lib/file-operations/search-worker.js +22 -12
  21. package/dist/lib/file-operations/tree.js +43 -34
  22. package/dist/lib/fs-helpers.js +61 -124
  23. package/dist/lib/observability.js +29 -28
  24. package/dist/lib/path-format.d.ts +1 -0
  25. package/dist/lib/path-format.js +7 -0
  26. package/dist/lib/path-policy.js +22 -20
  27. package/dist/lib/path-validation.js +13 -7
  28. package/dist/lib/resource-store.d.ts +2 -0
  29. package/dist/lib/resource-store.js +26 -5
  30. package/dist/lib/type-guards.d.ts +1 -0
  31. package/dist/lib/type-guards.js +3 -0
  32. package/dist/prompts.d.ts +1 -5
  33. package/dist/prompts.js +9 -16
  34. package/dist/resources.d.ts +1 -5
  35. package/dist/resources.js +12 -26
  36. package/dist/schemas.d.ts +213 -30
  37. package/dist/schemas.js +52 -90
  38. package/dist/server.js +85 -44
  39. package/dist/tools/apply-patch.js +24 -24
  40. package/dist/tools/calculate-hash.js +42 -45
  41. package/dist/tools/create-directory.js +18 -21
  42. package/dist/tools/delete-file.js +36 -39
  43. package/dist/tools/diff-files.js +16 -21
  44. package/dist/tools/edit-file.js +16 -20
  45. package/dist/tools/list-directory.js +25 -25
  46. package/dist/tools/move-file.js +18 -21
  47. package/dist/tools/read-multiple.js +56 -68
  48. package/dist/tools/read.js +27 -32
  49. package/dist/tools/replace-in-files.js +28 -35
  50. package/dist/tools/roots.js +9 -10
  51. package/dist/tools/search-content.js +74 -74
  52. package/dist/tools/search-files.js +45 -52
  53. package/dist/tools/shared.d.ts +44 -6
  54. package/dist/tools/shared.js +86 -64
  55. package/dist/tools/stat-many.js +45 -68
  56. package/dist/tools/stat.js +11 -39
  57. package/dist/tools/task-support.d.ts +9 -1
  58. package/dist/tools/task-support.js +86 -81
  59. package/dist/tools/tree.js +13 -30
  60. package/dist/tools/write-file.js +18 -21
  61. package/dist/tools.js +23 -18
  62. package/package.json +6 -7
@@ -4,6 +4,7 @@ import { isProbablyBinary } from '../fs-helpers.js';
4
4
  import { startPerfMeasure } from '../observability.js';
5
5
  import { buildMatcher, scanFileInWorker } from './search-content.js';
6
6
  const matcherCache = new Map();
7
+ const MAX_MATCHER_CACHE_SIZE = 100;
7
8
  function getMatcherCacheKey(pattern, options) {
8
9
  const cs = options.caseSensitive ? '1' : '0';
9
10
  const ww = options.wholeWord ? '1' : '0';
@@ -14,19 +15,25 @@ function getCachedMatcher(pattern, options) {
14
15
  const key = getMatcherCacheKey(pattern, options);
15
16
  const cached = matcherCache.get(key);
16
17
  if (cached) {
17
- matcherCache.delete(key);
18
- matcherCache.set(key, cached);
18
+ refreshMatcherCacheEntry(key, cached);
19
19
  return cached;
20
20
  }
21
21
  const matcher = buildMatcher(pattern, options);
22
+ refreshMatcherCacheEntry(key, matcher);
23
+ evictOldestMatcherIfNeeded();
24
+ return matcher;
25
+ }
26
+ function refreshMatcherCacheEntry(key, matcher) {
27
+ matcherCache.delete(key);
22
28
  matcherCache.set(key, matcher);
23
- if (matcherCache.size > 100) {
24
- const firstKey = matcherCache.keys().next().value;
25
- if (firstKey !== undefined) {
26
- matcherCache.delete(firstKey);
27
- }
29
+ }
30
+ function evictOldestMatcherIfNeeded() {
31
+ if (matcherCache.size <= MAX_MATCHER_CACHE_SIZE)
32
+ return;
33
+ const firstKey = matcherCache.keys().next().value;
34
+ if (firstKey !== undefined) {
35
+ matcherCache.delete(firstKey);
28
36
  }
29
- return matcher;
30
37
  }
31
38
  const cancelledRequests = new Set();
32
39
  const activeRequests = new Set();
@@ -45,6 +52,11 @@ function consumeCancelled(id) {
45
52
  cancelledRequests.delete(id);
46
53
  return true;
47
54
  }
55
+ function markCancelledIfActive(id) {
56
+ if (activeRequests.has(id)) {
57
+ cancelledRequests.add(id);
58
+ }
59
+ }
48
60
  function buildScanResponse(id, result) {
49
61
  return {
50
62
  type: 'result',
@@ -97,14 +109,12 @@ function handleMessage(message) {
97
109
  void handleScanRequest(message);
98
110
  break;
99
111
  case 'cancel':
100
- if (activeRequests.has(message.id)) {
101
- cancelledRequests.add(message.id);
102
- }
112
+ markCancelledIfActive(message.id);
103
113
  break;
104
114
  case 'shutdown':
105
115
  shuttingDown = true;
106
116
  for (const id of activeRequests) {
107
- cancelledRequests.add(id);
117
+ markCancelledIfActive(id);
108
118
  }
109
119
  maybeFinishShutdown();
110
120
  break;
@@ -1,10 +1,11 @@
1
1
  import * as path from 'node:path';
2
2
  import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_TIMEOUT_MS, } from '../constants.js';
3
3
  import { createTimedAbortSignal } from '../fs-helpers.js';
4
+ import { toPosixPath } from '../path-format.js';
4
5
  import { isSensitivePath } from '../path-policy.js';
5
6
  import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
6
7
  import { isIgnoredByGitignore, loadRootGitignore } from './gitignore.js';
7
- import { globEntries } from './glob-engine.js';
8
+ import { globEntries, resolveEntryType } from './glob-engine.js';
8
9
  function toSafeNonNegativeInt(value, fallback) {
9
10
  if (typeof value !== 'number' || !Number.isFinite(value))
10
11
  return fallback;
@@ -31,23 +32,23 @@ function normalizeOptions(options) {
31
32
  timeoutMs: toSafePositiveInt(options.timeoutMs, DEFAULT_SEARCH_TIMEOUT_MS),
32
33
  };
33
34
  }
34
- function resolveEntryType(dirent) {
35
- if (dirent.isDirectory())
36
- return 'directory';
37
- if (dirent.isSymbolicLink())
38
- return 'symlink';
39
- if (dirent.isFile())
40
- return 'file';
41
- return 'other';
42
- }
43
35
  function ensureParentNodes(rootNode, nodeByPath, relativePath) {
44
- const normalized = relativePath.replace(/\\/gu, '/');
36
+ const normalized = toPosixPath(relativePath);
45
37
  if (normalized.length === 0 || normalized === '.')
46
38
  return rootNode;
47
- const segments = normalized.split('/').filter((seg) => seg.length > 0);
39
+ const segments = [];
40
+ for (const segment of normalized.split('/')) {
41
+ if (segment.length > 0) {
42
+ segments.push(segment);
43
+ }
44
+ }
45
+ const parentSegmentCount = Math.max(0, segments.length - 1);
48
46
  let current = rootNode;
49
47
  let currentPath = '';
50
- for (const segment of segments.slice(0, Math.max(0, segments.length - 1))) {
48
+ for (let index = 0; index < parentSegmentCount; index += 1) {
49
+ const segment = segments[index];
50
+ if (!segment)
51
+ continue;
51
52
  currentPath =
52
53
  currentPath.length === 0
53
54
  ? segment
@@ -71,23 +72,24 @@ function ensureParentNodes(rootNode, nodeByPath, relativePath) {
71
72
  function sortTree(node) {
72
73
  if (!node.children)
73
74
  return;
74
- node.children.sort((a, b) => {
75
- const typeRank = (t) => {
76
- if (t === 'directory')
77
- return 0;
78
- if (t === 'file')
79
- return 1;
80
- return 2;
81
- };
82
- const diff = typeRank(a.type) - typeRank(b.type);
83
- if (diff !== 0)
84
- return diff;
85
- return a.name.localeCompare(b.name);
86
- });
75
+ node.children.sort(compareTreeEntries);
87
76
  for (const child of node.children) {
88
77
  sortTree(child);
89
78
  }
90
79
  }
80
+ function compareTreeEntries(a, b) {
81
+ const diff = getTreeTypeRank(a.type) - getTreeTypeRank(b.type);
82
+ if (diff !== 0)
83
+ return diff;
84
+ return a.name.localeCompare(b.name);
85
+ }
86
+ function getTreeTypeRank(type) {
87
+ if (type === 'directory')
88
+ return 0;
89
+ if (type === 'file')
90
+ return 1;
91
+ return 2;
92
+ }
91
93
  function getStopReason(signal, totalEntries, maxEntries) {
92
94
  if (signal.aborted) {
93
95
  return 'aborted';
@@ -97,11 +99,11 @@ function getStopReason(signal, totalEntries, maxEntries) {
97
99
  }
98
100
  return undefined;
99
101
  }
100
- async function resolveTreeEntry(entry, root, rootNormalized, gitignoreMatcher, signal) {
102
+ async function resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal) {
101
103
  const type = resolveEntryType(entry.dirent);
102
104
  if (type !== 'symlink') {
103
105
  const normalized = normalizePath(entry.path);
104
- if (!isPathWithinDirectories(normalized, [rootNormalized])) {
106
+ if (!isPathWithinDirectories(normalized, rootDirectories)) {
105
107
  return null;
106
108
  }
107
109
  if (isSensitivePath(entry.path, normalized)) {
@@ -126,7 +128,7 @@ async function resolveTreeEntry(entry, root, rootNormalized, gitignoreMatcher, s
126
128
  return null;
127
129
  }
128
130
  const relative = path.relative(root, entry.path) || path.basename(entry.path);
129
- const relativePosix = relative.replace(/\\/gu, '/');
131
+ const relativePosix = toPosixPath(relative);
130
132
  const name = path.basename(entry.path);
131
133
  return { type, relativePosix, name };
132
134
  }
@@ -154,7 +156,10 @@ function upsertChildNode(parent, nodeByPath, resolved, childPathIndexByParent) {
154
156
  parent.children ??= [];
155
157
  let seen = childPathIndexByParent.get(parent);
156
158
  if (!seen) {
157
- seen = new Set(parent.children.map((entry) => entry.relativePath));
159
+ seen = new Set();
160
+ for (const entry of parent.children) {
161
+ seen.add(entry.relativePath);
162
+ }
158
163
  childPathIndexByParent.set(parent, seen);
159
164
  }
160
165
  const key = child.relativePath;
@@ -201,9 +206,12 @@ export function formatTreeAscii(tree) {
201
206
  nextPrefix = `${prefix}${continuation}`;
202
207
  }
203
208
  const count = node.children.length;
204
- node.children.forEach((child, index) => {
205
- walk(child, nextPrefix, index === count - 1, false);
206
- });
209
+ for (let index = 0; index < count; index += 1) {
210
+ const child = node.children[index];
211
+ if (child) {
212
+ walk(child, nextPrefix, index === count - 1, false);
213
+ }
214
+ }
207
215
  };
208
216
  walk(tree, '', true, true);
209
217
  return lines.join('\n');
@@ -213,6 +221,7 @@ export async function treeDirectory(dirPath, options = {}) {
213
221
  const { signal, cleanup } = createTimedAbortSignal(options.signal, normalized.timeoutMs);
214
222
  const root = await validateExistingDirectory(dirPath, signal);
215
223
  const rootNormalized = normalizePath(root);
224
+ const rootDirectories = [rootNormalized];
216
225
  try {
217
226
  const excludePatterns = normalized.includeIgnored
218
227
  ? []
@@ -249,7 +258,7 @@ export async function treeDirectory(dirPath, options = {}) {
249
258
  truncated = true;
250
259
  break;
251
260
  }
252
- const resolved = await resolveTreeEntry(entry, root, rootNormalized, gitignoreMatcher, signal);
261
+ const resolved = await resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal);
253
262
  if (!resolved) {
254
263
  continue;
255
264
  }
@@ -11,11 +11,20 @@ import { validateExistingPath } from './path-validation.js';
11
11
  function createAbortError(message = 'Operation aborted') {
12
12
  return new DOMException(message, 'AbortError');
13
13
  }
14
+ const SHARED_NOOP_SIGNAL = new AbortController().signal;
14
15
  function normalizeAbortReason(reason, message) {
15
16
  if (reason instanceof Error)
16
17
  return reason;
17
18
  return createAbortError(message);
18
19
  }
20
+ function isFiniteNumber(value) {
21
+ return typeof value === 'number' && Number.isFinite(value);
22
+ }
23
+ function normalizeUnknownError(error) {
24
+ return error instanceof Error
25
+ ? error
26
+ : new Error(formatUnknownErrorMessage(error));
27
+ }
19
28
  export function assertNotAborted(signal, message) {
20
29
  if (!signal)
21
30
  return;
@@ -83,15 +92,13 @@ export function withAbort(promise, signal) {
83
92
  })
84
93
  .catch((error) => {
85
94
  finish(() => {
86
- reject(error instanceof Error
87
- ? error
88
- : new Error(formatUnknownErrorMessage(error)));
95
+ reject(normalizeUnknownError(error));
89
96
  });
90
97
  });
91
98
  });
92
99
  }
93
100
  export function createTimedAbortSignal(baseSignal, timeoutMs) {
94
- const timeoutSignal = typeof timeoutMs === 'number' && Number.isFinite(timeoutMs)
101
+ const timeoutSignal = isFiniteNumber(timeoutMs)
95
102
  ? AbortSignal.timeout(timeoutMs)
96
103
  : undefined;
97
104
  if (baseSignal && timeoutSignal) {
@@ -109,8 +116,7 @@ export function createTimedAbortSignal(baseSignal, timeoutMs) {
109
116
  return createNoopSignal();
110
117
  }
111
118
  function createNoopSignal() {
112
- const controller = new AbortController();
113
- return { signal: controller.signal, cleanup: () => { } };
119
+ return { signal: SHARED_NOOP_SIGNAL, cleanup: () => { } };
114
120
  }
115
121
  function createForwardedSignal(baseSignal) {
116
122
  return { signal: baseSignal, cleanup: () => { } };
@@ -118,118 +124,56 @@ function createForwardedSignal(baseSignal) {
118
124
  function createParallelAbortError() {
119
125
  return createAbortError();
120
126
  }
121
- function createState(items, processor, concurrency, signal) {
122
- return {
123
- items,
124
- processor,
125
- concurrency,
126
- results: [],
127
- errors: [],
128
- nextIndex: 0,
129
- aborted: Boolean(signal?.aborted),
130
- inFlight: new Set(),
131
- };
132
- }
133
- function attachAbortListener(state, signal) {
134
- if (!signal || signal.aborted)
135
- return () => { };
136
- const onAbort = () => {
137
- state.aborted = true;
138
- };
139
- signal.addEventListener('abort', onAbort, { once: true });
140
- return () => {
141
- signal.removeEventListener('abort', onAbort);
142
- };
143
- }
144
- function createAbortPromise(signal) {
145
- if (!signal)
146
- return { cleanup: () => { } };
147
- if (signal.aborted)
148
- return { abortPromise: Promise.resolve(), cleanup: () => { } };
149
- let cleanup = () => { };
150
- const abortPromise = new Promise((resolve) => {
151
- const onAbort = () => {
152
- resolve();
153
- };
154
- signal.addEventListener('abort', onAbort, { once: true });
155
- cleanup = () => {
156
- signal.removeEventListener('abort', onAbort);
157
- };
158
- });
159
- return { abortPromise, cleanup };
160
- }
161
- function canStartNext(state) {
162
- return (!state.aborted &&
163
- state.inFlight.size < state.concurrency &&
164
- state.nextIndex < state.items.length);
165
- }
166
- async function createTask(item, index, state) {
167
- try {
168
- const result = await state.processor(item);
169
- state.results.push(result);
170
- }
171
- catch (reason) {
172
- const error = reason instanceof Error
173
- ? reason
174
- : new Error(formatUnknownErrorMessage(reason));
175
- state.errors.push({ index, error });
176
- }
177
- }
178
- function queueNextTask(state) {
179
- const index = state.nextIndex;
180
- state.nextIndex += 1;
181
- const item = state.items[index];
182
- if (item === undefined)
183
- return;
184
- const task = createTask(item, index, state);
185
- state.inFlight.add(task);
186
- void task.finally(() => {
187
- state.inFlight.delete(task);
188
- });
189
- }
190
- function startNextTasks(state) {
191
- while (canStartNext(state)) {
192
- queueNextTask(state);
193
- }
194
- }
195
- async function drainTasks(state, abortPromise) {
196
- startNextTasks(state);
197
- while (state.inFlight.size > 0) {
198
- if (abortPromise) {
199
- const nextTask = Promise.race(state.inFlight);
200
- await Promise.race([nextTask, abortPromise]);
201
- }
202
- else {
203
- await Promise.race(state.inFlight);
204
- }
205
- if (state.aborted)
206
- break;
207
- startNextTasks(state);
208
- }
209
- if (state.inFlight.size > 0) {
210
- await Promise.allSettled(state.inFlight);
211
- }
212
- }
213
127
  export async function processInParallel(items, processor, concurrency = PARALLEL_CONCURRENCY, signal) {
214
- const { abortPromise, cleanup: cleanupAbortPromise } = createAbortPromise(signal);
215
- if (items.length === 0) {
216
- cleanupAbortPromise();
128
+ if (items.length === 0)
217
129
  return { results: [], errors: [] };
218
- }
219
130
  const effectiveConcurrency = normalizeConcurrency(concurrency);
220
- const state = createState(items, processor, effectiveConcurrency, signal);
221
- const detachAbort = attachAbortListener(state, signal);
222
- try {
223
- await drainTasks(state, abortPromise);
224
- }
225
- finally {
226
- detachAbort();
227
- cleanupAbortPromise();
131
+ // Pre-allocate slots by index to guarantee input-order output.
132
+ const resultSlots = new Array(items.length);
133
+ const errors = [];
134
+ if (signal?.aborted)
135
+ throw createParallelAbortError();
136
+ let nextIndex = 0;
137
+ const next = async () => {
138
+ while (nextIndex < items.length) {
139
+ if (signal?.aborted)
140
+ throw createParallelAbortError();
141
+ const index = nextIndex++;
142
+ // Check again because another worker might have incremented past length
143
+ if (index >= items.length)
144
+ break;
145
+ const item = items[index];
146
+ try {
147
+ const result = await processor(item);
148
+ if (signal?.aborted)
149
+ throw createParallelAbortError();
150
+ resultSlots[index] = result;
151
+ }
152
+ catch (error) {
153
+ if (signal?.aborted)
154
+ throw createParallelAbortError();
155
+ errors.push({
156
+ index,
157
+ error: normalizeUnknownError(error),
158
+ });
159
+ }
160
+ }
161
+ };
162
+ const workerCount = Math.min(items.length, effectiveConcurrency);
163
+ const workers = new Array(workerCount);
164
+ for (let index = 0; index < workerCount; index += 1) {
165
+ workers[index] = next();
228
166
  }
229
- if (state.aborted) {
167
+ await Promise.allSettled(workers);
168
+ if (signal?.aborted)
230
169
  throw createParallelAbortError();
170
+ const results = [];
171
+ for (const slot of resultSlots) {
172
+ if (slot !== undefined) {
173
+ results.push(slot);
174
+ }
231
175
  }
232
- return { results: state.results, errors: state.errors };
176
+ return { results, errors };
233
177
  }
234
178
  export function getFileType(stats) {
235
179
  if (stats.isFile())
@@ -307,12 +251,6 @@ function validateReadOptions(options) {
307
251
  if (hasEnd && !hasStart) {
308
252
  throw new McpError(ErrorCode.E_INVALID_INPUT, 'endLine requires startLine');
309
253
  }
310
- if (options.startLine !== undefined && options.startLine < 1) {
311
- throw new McpError(ErrorCode.E_INVALID_INPUT, 'startLine must be at least 1');
312
- }
313
- if (options.endLine !== undefined && options.endLine < 1) {
314
- throw new McpError(ErrorCode.E_INVALID_INPUT, 'endLine must be at least 1');
315
- }
316
254
  if (options.startLine !== undefined &&
317
255
  options.endLine !== undefined &&
318
256
  options.endLine < options.startLine) {
@@ -326,7 +264,6 @@ function normalizeOptions(options) {
326
264
  maxSize: Math.min(options.maxSize ?? MAX_TEXT_FILE_SIZE, MAX_TEXT_FILE_SIZE),
327
265
  skipBinary: options.skipBinary ?? false,
328
266
  };
329
- assertPositiveSafeIntegerOption('maxSize', normalized.maxSize, 'maxSize must be at least 1');
330
267
  if (options.head !== undefined) {
331
268
  normalized.head = options.head;
332
269
  }
@@ -590,12 +527,12 @@ async function readFullResult(handle, validPath, filePath, stats, normalized) {
590
527
  async function readByMode(handle, validPath, filePath, stats, normalized) {
591
528
  const mode = resolveReadMode(normalized);
592
529
  if (mode === 'head') {
593
- return await readHeadResult(handle, validPath, filePath, normalized);
530
+ return readHeadResult(handle, validPath, filePath, normalized);
594
531
  }
595
532
  if (mode === 'range') {
596
- return await readRangeResult(handle, validPath, filePath, normalized);
533
+ return readRangeResult(handle, validPath, filePath, normalized);
597
534
  }
598
- return await readFullResult(handle, validPath, filePath, stats, normalized);
535
+ return readFullResult(handle, validPath, filePath, stats, normalized);
599
536
  }
600
537
  function assertFileStats(filePath, stats) {
601
538
  if (!stats.isFile()) {
@@ -620,14 +557,14 @@ async function readFileWithStatsInternal(filePath, validPath, stats, normalized)
620
557
  }
621
558
  export async function readFileWithStats(filePath, validPath, stats, options = {}) {
622
559
  const normalized = prepareReadOptions(options);
623
- return await readFileWithStatsInternal(filePath, validPath, stats, normalized);
560
+ return readFileWithStatsInternal(filePath, validPath, stats, normalized);
624
561
  }
625
562
  export async function readFile(filePath, options = {}) {
626
563
  const normalized = prepareReadOptions(options);
627
564
  const validPath = await validateExistingPath(filePath, normalized.signal);
628
565
  assertNotAborted(normalized.signal);
629
566
  const stats = await withAbort(fsp.stat(validPath), normalized.signal);
630
- return await readFileWithStatsInternal(filePath, validPath, stats, normalized);
567
+ return readFileWithStatsInternal(filePath, validPath, stats, normalized);
631
568
  }
632
569
  export async function atomicWriteFile(filePath, content, options = {}) {
633
570
  const { encoding = 'utf-8', signal } = options;
@@ -2,14 +2,16 @@ import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import { hash } from 'node:crypto';
3
3
  import { channel, tracingChannel } from 'node:diagnostics_channel';
4
4
  import { monitorEventLoopDelay, performance, PerformanceObserver, } from 'node:perf_hooks';
5
+ import { isRecord } from './type-guards.js';
5
6
  // --- Configuration ---
6
7
  const ENV = process.env;
8
+ let _cachedConfig;
7
9
  function readConfig() {
8
- return {
10
+ return (_cachedConfig ??= {
9
11
  enabled: isTrue(ENV['FS_CONTEXT_DIAGNOSTICS']),
10
12
  detail: parseDetail(ENV['FS_CONTEXT_DIAGNOSTICS_DETAIL']),
11
13
  logToolErrors: isTrue(ENV['FS_CONTEXT_TOOL_LOG_ERRORS']),
12
- };
14
+ });
13
15
  }
14
16
  function isTrue(val) {
15
17
  const norm = val?.trim().toLowerCase();
@@ -47,11 +49,8 @@ const toolContext = new AsyncLocalStorage({
47
49
  let perfObserver;
48
50
  let traceCounter = 0;
49
51
  // --- Helpers: Result Analysis ---
50
- function isObject(v) {
51
- return typeof v === 'object' && v !== null;
52
- }
53
52
  function extractOutcome(result) {
54
- if (!isObject(result)) {
53
+ if (!isRecord(result)) {
55
54
  return { ok: true };
56
55
  }
57
56
  if (result['isError'] === true) {
@@ -64,7 +63,7 @@ function extractOutcome(result) {
64
63
  return { ok: false, error: extractErrorMessage(result) };
65
64
  }
66
65
  const content = result['structuredContent'];
67
- if (isObject(content) && typeof content['ok'] === 'boolean') {
66
+ if (isRecord(content) && typeof content['ok'] === 'boolean') {
68
67
  if (content['ok'])
69
68
  return { ok: true };
70
69
  const err = extractResultError(content);
@@ -77,17 +76,17 @@ function extractErrorMessage(source) {
77
76
  return source;
78
77
  if (source instanceof Error)
79
78
  return source.message;
80
- if (isObject(source)) {
79
+ if (isRecord(source)) {
81
80
  const struct = source['structuredContent'];
82
- if (isObject(struct)) {
81
+ if (isRecord(struct)) {
83
82
  const err = struct['error'];
84
- if (isObject(err) && typeof err['message'] === 'string')
83
+ if (isRecord(err) && typeof err['message'] === 'string')
85
84
  return err['message'];
86
85
  }
87
86
  if (typeof source['message'] === 'string')
88
87
  return source['message'];
89
88
  const errObj = source['error'];
90
- if (isObject(errObj) && typeof errObj['message'] === 'string') {
89
+ if (isRecord(errObj) && typeof errObj['message'] === 'string') {
91
90
  return errObj['message'];
92
91
  }
93
92
  }
@@ -100,7 +99,7 @@ function extractErrorMessage(source) {
100
99
  }
101
100
  function extractResultError(structured) {
102
101
  const err = structured['error'];
103
- return isObject(err) && typeof err['message'] === 'string'
102
+ return isRecord(err) && typeof err['message'] === 'string'
104
103
  ? err['message']
105
104
  : undefined;
106
105
  }
@@ -216,6 +215,10 @@ function normalizeContext(ctx) {
216
215
  }
217
216
  return { ...ctx, path: normalized };
218
217
  }
218
+ function clearMeasureMarks(startMark, endMark) {
219
+ performance.clearMarks(startMark);
220
+ performance.clearMarks(endMark);
221
+ }
219
222
  export function startPerfMeasure(name, detail) {
220
223
  if (!readConfig().enabled || !CHANNELS.perf.hasSubscribers)
221
224
  return undefined;
@@ -245,14 +248,12 @@ export function startPerfMeasure(name, detail) {
245
248
  });
246
249
  }
247
250
  finally {
248
- performance.clearMarks(startMark);
249
- performance.clearMarks(endMark);
251
+ clearMeasureMarks(startMark, endMark);
250
252
  }
251
253
  });
252
254
  }
253
255
  catch {
254
- performance.clearMarks(startMark);
255
- performance.clearMarks(endMark);
256
+ clearMeasureMarks(startMark, endMark);
256
257
  }
257
258
  };
258
259
  }
@@ -291,15 +292,15 @@ async function runAndObserve(tool, run, pubTool, pubPerf, logErrors, pathVal) {
291
292
  if (pubTool)
292
293
  publishToolStart(tool, pathVal);
293
294
  let result;
294
- let ok = false;
295
- let errorMsg;
295
+ const obs = { ok: false, errorMsg: undefined };
296
296
  try {
297
297
  result = await run();
298
- ({ ok, error: errorMsg } = extractOutcome(result));
298
+ const { ok, error } = extractOutcome(result);
299
+ obs.ok = ok;
300
+ obs.errorMsg = error;
299
301
  }
300
302
  catch (err) {
301
- ok = false;
302
- errorMsg = extractErrorMessage(err);
303
+ obs.errorMsg = extractErrorMessage(err);
303
304
  throw err;
304
305
  }
305
306
  finally {
@@ -308,10 +309,10 @@ async function runAndObserve(tool, run, pubTool, pubPerf, logErrors, pathVal) {
308
309
  if (pubPerf && eluStart)
309
310
  publishPerfEnd(tool, durationMs, eluStart, loopMonitor);
310
311
  if (pubTool)
311
- publishToolEnd(tool, ok, durationMs, errorMsg);
312
- updateMetrics(tool, ok, durationMs);
313
- if (logErrors && !ok)
314
- logError(tool, durationMs, errorMsg);
312
+ publishToolEnd(tool, obs.ok, durationMs, obs.errorMsg);
313
+ updateMetrics(tool, obs.ok, durationMs);
314
+ if (logErrors && !obs.ok)
315
+ logError(tool, durationMs, obs.errorMsg);
315
316
  }
316
317
  return result;
317
318
  }
@@ -322,10 +323,10 @@ export async function withToolDiagnostics(tool, run, options) {
322
323
  tool,
323
324
  ...(options?.path ? { path: options.path } : {}),
324
325
  };
325
- return await toolContext.run(context, async () => {
326
+ return toolContext.run(context, async () => {
326
327
  if (!config.enabled) {
327
328
  if (!config.logToolErrors)
328
- return await run();
329
+ return run();
329
330
  const start = performance.now();
330
331
  try {
331
332
  const res = await run();
@@ -356,7 +357,7 @@ export async function withToolDiagnostics(tool, run, options) {
356
357
  throw e;
357
358
  }
358
359
  }
359
- return await runAndObserve(tool, run, pubTool, pubPerf, config.logToolErrors, normalizedPath);
360
+ return runAndObserve(tool, run, pubTool, pubPerf, config.logToolErrors, normalizedPath);
360
361
  });
361
362
  }
362
363
  function logError(tool, durationMs, msg) {
@@ -0,0 +1 @@
1
+ export declare function toPosixPath(value: string): string;
@@ -0,0 +1,7 @@
1
+ const WINDOWS_PATH_SEPARATOR = '\\';
2
+ const POSIX_PATH_SEPARATOR = '/';
3
+ export function toPosixPath(value) {
4
+ return value.includes(WINDOWS_PATH_SEPARATOR)
5
+ ? value.replace(/\\/gu, POSIX_PATH_SEPARATOR)
6
+ : value;
7
+ }