@j0hanz/filesystem-mcp 1.14.0 → 1.15.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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -71
  3. package/dist/cli.js +2 -2
  4. package/dist/completions.js +54 -51
  5. package/dist/config.d.ts +13 -13
  6. package/dist/config.js +12 -12
  7. package/dist/index.js +1 -1
  8. package/dist/lib/abort.d.ts +7 -0
  9. package/dist/lib/abort.js +81 -0
  10. package/dist/lib/constants.d.ts +3 -1
  11. package/dist/lib/constants.js +8 -2
  12. package/dist/lib/errors.d.ts +7 -3
  13. package/dist/lib/errors.js +59 -39
  14. package/dist/lib/file-operations/core.d.ts +3 -3
  15. package/dist/lib/file-operations/core.js +23 -20
  16. package/dist/lib/file-operations/metadata.d.ts +2 -2
  17. package/dist/lib/file-operations/metadata.js +60 -19
  18. package/dist/lib/file-operations/search.js +83 -84
  19. package/dist/lib/file-operations/traversal.js +13 -15
  20. package/dist/lib/fs-helpers.d.ts +3 -10
  21. package/dist/lib/fs-helpers.js +20 -98
  22. package/dist/lib/globs.js +1 -1
  23. package/dist/lib/logger.d.ts +28 -0
  24. package/dist/lib/logger.js +91 -0
  25. package/dist/lib/observability.d.ts +7 -0
  26. package/dist/lib/observability.js +19 -9
  27. package/dist/lib/paths.js +55 -55
  28. package/dist/lib/resource-store.js +4 -4
  29. package/dist/lib/utils.d.ts +0 -12
  30. package/dist/lib/utils.js +0 -13
  31. package/dist/resources/generated-instructions.js +40 -31
  32. package/dist/resources/tool-catalog.js +32 -26
  33. package/dist/resources/tool-info.js +34 -29
  34. package/dist/resources/workflows.js +39 -18
  35. package/dist/resources.d.ts +1 -1
  36. package/dist/resources.js +4 -4
  37. package/dist/schemas.d.ts +66 -66
  38. package/dist/schemas.js +21 -44
  39. package/dist/server/bootstrap.d.ts +12 -11
  40. package/dist/server/bootstrap.js +95 -86
  41. package/dist/server/roots-manager.d.ts +5 -2
  42. package/dist/server/roots-manager.js +8 -6
  43. package/dist/server/task-store.d.ts +10 -0
  44. package/dist/server/task-store.js +73 -0
  45. package/dist/tools/apply-patch.js +26 -18
  46. package/dist/tools/calculate-hash.js +11 -22
  47. package/dist/tools/create-directory.js +10 -8
  48. package/dist/tools/delete-file.js +17 -15
  49. package/dist/tools/diff-files.js +15 -15
  50. package/dist/tools/edit-file.js +7 -4
  51. package/dist/tools/list-directory.js +6 -6
  52. package/dist/tools/move-file.js +98 -81
  53. package/dist/tools/read-multiple.js +4 -4
  54. package/dist/tools/read.js +5 -5
  55. package/dist/tools/replace-in-files.js +20 -23
  56. package/dist/tools/roots.js +1 -1
  57. package/dist/tools/search-content.js +32 -41
  58. package/dist/tools/search-files.js +54 -41
  59. package/dist/tools/shared.d.ts +3 -0
  60. package/dist/tools/shared.js +70 -28
  61. package/dist/tools/stat-many.js +11 -7
  62. package/dist/tools/stat.js +4 -4
  63. package/dist/tools/task-support.d.ts +10 -9
  64. package/dist/tools/task-support.js +94 -23
  65. package/dist/tools/tree.js +3 -3
  66. package/dist/tools/write-file.js +10 -7
  67. package/package.json +9 -9
package/dist/config.js CHANGED
@@ -1,16 +1,16 @@
1
1
  export const ErrorCode = {
2
- E_ACCESS_DENIED: 'E_ACCESS_DENIED',
3
- E_NOT_FOUND: 'E_NOT_FOUND',
4
- E_NOT_FILE: 'E_NOT_FILE',
5
- E_NOT_DIRECTORY: 'E_NOT_DIRECTORY',
6
- E_TOO_LARGE: 'E_TOO_LARGE',
7
- E_TIMEOUT: 'E_TIMEOUT',
8
- E_CANCELLED: 'E_CANCELLED',
9
- E_INVALID_PATTERN: 'E_INVALID_PATTERN',
10
- E_INVALID_INPUT: 'E_INVALID_INPUT',
11
- E_PERMISSION_DENIED: 'E_PERMISSION_DENIED',
12
- E_SYMLINK_NOT_ALLOWED: 'E_SYMLINK_NOT_ALLOWED',
13
- E_UNKNOWN: 'E_UNKNOWN',
2
+ ACCESS_DENIED: 'ACCESS_DENIED',
3
+ NOT_FOUND: 'NOT_FOUND',
4
+ NOT_FILE: 'NOT_FILE',
5
+ NOT_DIRECTORY: 'NOT_DIRECTORY',
6
+ TOO_LARGE: 'TOO_LARGE',
7
+ TIMEOUT: 'TIMEOUT',
8
+ CANCELLED: 'CANCELLED',
9
+ INVALID_PATTERN: 'INVALID_PATTERN',
10
+ INVALID_INPUT: 'INVALID_INPUT',
11
+ PERMISSION_DENIED: 'PERMISSION_DENIED',
12
+ SYMLINK_NOT_ALLOWED: 'SYMLINK_NOT_ALLOWED',
13
+ UNKNOWN: 'UNKNOWN',
14
14
  };
15
15
  const BYTE_UNIT_LABELS = ['B', 'KB', 'MB', 'GB', 'TB'];
16
16
  export function formatBytes(bytes) {
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import process from 'node:process';
3
+ import { createTimedAbortSignal } from './lib/abort.js';
3
4
  import { DEFAULT_SEARCH_TIMEOUT_MS } from './lib/constants.js';
4
5
  import { formatUnknownErrorMessage } from './lib/errors.js';
5
- import { createTimedAbortSignal } from './lib/fs-helpers.js';
6
6
  import { setAllowedDirectoriesResolved } from './lib/paths.js';
7
7
  import { CliExitError, parseArgs } from './cli.js';
8
8
  import { createServer, startHttpServer, startServer } from './server.js';
@@ -0,0 +1,7 @@
1
+ export declare function assertNotAborted(signal?: AbortSignal, message?: string): void;
2
+ export declare function withAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T>;
3
+ export declare function createTimedAbortSignal(baseSignal: AbortSignal | undefined, timeoutMs?: number): {
4
+ signal: AbortSignal;
5
+ cleanup: () => void;
6
+ };
7
+ export declare function withTimedAbortSignal<T>(baseSignal: AbortSignal | undefined, timeoutMs: number | undefined, run: (signal: AbortSignal) => Promise<T>): Promise<T>;
@@ -0,0 +1,81 @@
1
+ import { normalizeUnknownError } from './errors.js';
2
+ function createAbortError(message = 'Operation aborted') {
3
+ return new DOMException(message, 'AbortError');
4
+ }
5
+ const SHARED_NOOP_SIGNAL = new AbortController().signal;
6
+ function normalizeAbortReason(reason, message) {
7
+ if (reason instanceof Error)
8
+ return reason;
9
+ return createAbortError(message);
10
+ }
11
+ function isFiniteNumber(value) {
12
+ return typeof value === 'number' && Number.isFinite(value);
13
+ }
14
+ export function assertNotAborted(signal, message) {
15
+ if (!signal)
16
+ return;
17
+ try {
18
+ signal.throwIfAborted();
19
+ }
20
+ catch (reason) {
21
+ throw normalizeAbortReason(reason, message);
22
+ }
23
+ }
24
+ function getAbortError(signal, message) {
25
+ try {
26
+ signal.throwIfAborted();
27
+ }
28
+ catch (reason) {
29
+ return normalizeAbortReason(reason, message);
30
+ }
31
+ return createAbortError(message);
32
+ }
33
+ export function withAbort(promise, signal) {
34
+ if (!signal)
35
+ return promise;
36
+ signal.throwIfAborted();
37
+ return new Promise((resolve, reject) => {
38
+ const onAbort = () => {
39
+ reject(getAbortError(signal));
40
+ };
41
+ if (signal.aborted) {
42
+ onAbort();
43
+ return;
44
+ }
45
+ signal.addEventListener('abort', onAbort, { once: true });
46
+ promise.then((value) => {
47
+ signal.removeEventListener('abort', onAbort);
48
+ resolve(value);
49
+ }, (error) => {
50
+ signal.removeEventListener('abort', onAbort);
51
+ reject(normalizeUnknownError(error));
52
+ });
53
+ });
54
+ }
55
+ export function createTimedAbortSignal(baseSignal, timeoutMs) {
56
+ const timeoutSignal = isFiniteNumber(timeoutMs)
57
+ ? AbortSignal.timeout(timeoutMs)
58
+ : undefined;
59
+ if (baseSignal && timeoutSignal) {
60
+ return {
61
+ signal: AbortSignal.any([baseSignal, timeoutSignal]),
62
+ cleanup: () => { },
63
+ };
64
+ }
65
+ if (baseSignal) {
66
+ return { signal: baseSignal, cleanup: () => { } };
67
+ }
68
+ if (timeoutSignal) {
69
+ return { signal: timeoutSignal, cleanup: () => { } };
70
+ }
71
+ return { signal: SHARED_NOOP_SIGNAL, cleanup: () => { } };
72
+ }
73
+ export async function withTimedAbortSignal(baseSignal, timeoutMs, run) {
74
+ const { signal, cleanup } = createTimedAbortSignal(baseSignal, timeoutMs);
75
+ try {
76
+ return await run(signal);
77
+ }
78
+ finally {
79
+ cleanup();
80
+ }
81
+ }
@@ -1,9 +1,11 @@
1
1
  export declare function parseTrueEnvFlag(value: string | undefined): boolean;
2
2
  export declare function parseEnvInt(envVar: string, defaultValue: number, min: number, max: number): number;
3
- export declare const DEFAULT_LOG_LEVEL: "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency";
3
+ export declare const DEFAULT_LOG_LEVEL: "error" | "debug" | "info" | "notice" | "warning" | "critical" | "alert" | "emergency";
4
4
  export declare const DEFAULT_TASK_TTL_MS: number;
5
5
  export declare const MAX_TASK_TTL_MS: number;
6
6
  export declare const MAX_CONCURRENT_TASKS: number;
7
+ export declare const TASK_CANCEL_POLL_MS = 2000;
8
+ export declare const TASK_POLL_INTERVAL_MS = 100;
7
9
  export declare const PARALLEL_CONCURRENCY: number;
8
10
  export declare const MAX_SEARCHABLE_FILE_SIZE: number;
9
11
  export declare const MAX_TEXT_FILE_SIZE: number;
@@ -1,4 +1,5 @@
1
1
  import { availableParallelism } from 'node:os';
2
+ import { Logger } from './logger.js';
2
3
  const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes', 'y', 'on']);
3
4
  const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'n', 'off']);
4
5
  export function parseTrueEnvFlag(value) {
@@ -9,7 +10,7 @@ export function parseTrueEnvFlag(value) {
9
10
  const KIB = 1024;
10
11
  const MIB = 1024 * KIB;
11
12
  function logInvalidEnvValue(envVar, value, expected, defaultValue) {
12
- console.error(`[WARNING] Invalid ${envVar} value: ${value} (must be ${expected}). Using default: ${String(defaultValue)}`);
13
+ Logger.warn(`Invalid ${envVar} value: ${value} (must be ${expected}). Using default: ${String(defaultValue)}`);
13
14
  }
14
15
  // Helper for parsing environment variables (only used for configurable values)
15
16
  export function parseEnvInt(envVar, defaultValue, min, max) {
@@ -66,7 +67,7 @@ function parseEnvLogLevel(envVar, defaultValue) {
66
67
  if (VALID_LOG_LEVELS.includes(normalized)) {
67
68
  return normalized;
68
69
  }
69
- console.error(`[WARNING] Invalid ${envVar} value: ${value} (must be ${VALID_LOG_LEVELS.join('|')}). Using default: ${defaultValue}`);
70
+ Logger.warn(`Invalid ${envVar} value: ${value} (must be ${VALID_LOG_LEVELS.join('|')}). Using default: ${defaultValue}`);
70
71
  return defaultValue;
71
72
  }
72
73
  export const DEFAULT_LOG_LEVEL = parseEnvLogLevel('FILESYSTEM_MCP_LOG_LEVEL', 'info');
@@ -74,6 +75,11 @@ export const DEFAULT_LOG_LEVEL = parseEnvLogLevel('FILESYSTEM_MCP_LOG_LEVEL', 'i
74
75
  export const DEFAULT_TASK_TTL_MS = 5 * 60 * 1000;
75
76
  export const MAX_TASK_TTL_MS = parseEnvInt('FILESYSTEM_MCP_MAX_TASK_TTL_MS', 60 * 60 * 1000, 1_000, 24 * 60 * 60 * 1000);
76
77
  export const MAX_CONCURRENT_TASKS = parseEnvInt('FILESYSTEM_MCP_MAX_CONCURRENT_TASKS', 100, 1, 10_000);
78
+ // How often (ms) a background task checks the store for client cancellation.
79
+ export const TASK_CANCEL_POLL_MS = 2_000;
80
+ // Suggested poll interval (ms) returned to the SDK/client for automatic task polling.
81
+ // Filesystem operations complete quickly; a short interval avoids unnecessary latency.
82
+ export const TASK_POLL_INTERVAL_MS = 100;
77
83
  // Auto-tuned parallelism based on CPU cores (no env override)
78
84
  const BYTES_PER_PARALLEL_TASK = 64 * MIB;
79
85
  const BYTES_PER_SEARCH_WORKER = 128 * MIB;
@@ -15,9 +15,13 @@ export declare function isTimeoutLikeError(error: unknown): boolean;
15
15
  export declare class McpError extends Error {
16
16
  code: ErrorCode;
17
17
  path?: string | undefined;
18
- details?: Record<string, unknown> | undefined;
19
- constructor(code: ErrorCode, message: string, path?: string | undefined, details?: Record<string, unknown> | undefined, cause?: unknown);
18
+ details?: Record<string, unknown>;
19
+ constructor(code: ErrorCode, message: string, path?: string | undefined, details?: Record<string, unknown>, cause?: unknown);
20
+ static notFound(message: string, path?: string, details?: Record<string, unknown>, cause?: unknown): McpError;
21
+ static invalidInput(message: string, path?: string, details?: Record<string, unknown>, cause?: unknown): McpError;
22
+ static accessDenied(message: string, path?: string, details?: Record<string, unknown>, cause?: unknown): McpError;
23
+ static timeout(message: string, path?: string, details?: Record<string, unknown>, cause?: unknown): McpError;
20
24
  }
21
25
  export declare function createDetailedError(error: unknown, path?: string, additionalDetails?: Record<string, unknown>): DetailedError;
22
26
  export declare function formatDetailedError(error: DetailedError): string;
23
- export declare function getSuggestion(code: ErrorCode): string;
27
+ export declare function getSuggestion(code: ErrorCode): string | undefined;
@@ -1,6 +1,7 @@
1
1
  import { constants as osConstants } from 'node:os';
2
2
  import { getSystemErrorMap, getSystemErrorName, inspect } from 'node:util';
3
3
  import { ErrorCode, joinLines } from '../config.js';
4
+ import { getTraceContext } from './observability.js';
4
5
  export { ErrorCode };
5
6
  function isNativeError(error) {
6
7
  const candidate = Error;
@@ -100,20 +101,20 @@ export function normalizeUnknownError(error) {
100
101
  : new Error(formatUnknownErrorMessage(error));
101
102
  }
102
103
  const NODE_ERROR_CODE_MAP = {
103
- ENOENT: ErrorCode.E_NOT_FOUND,
104
- EACCES: ErrorCode.E_PERMISSION_DENIED,
105
- EPERM: ErrorCode.E_PERMISSION_DENIED,
106
- ENOTDIR: ErrorCode.E_NOT_DIRECTORY,
107
- EISDIR: ErrorCode.E_NOT_FILE,
108
- ELOOP: ErrorCode.E_SYMLINK_NOT_ALLOWED,
109
- ENAMETOOLONG: ErrorCode.E_INVALID_INPUT,
110
- ETIMEDOUT: ErrorCode.E_TIMEOUT,
111
- EMFILE: ErrorCode.E_TIMEOUT,
112
- ENFILE: ErrorCode.E_TIMEOUT,
113
- EBUSY: ErrorCode.E_PERMISSION_DENIED,
114
- ENOTEMPTY: ErrorCode.E_NOT_DIRECTORY,
115
- EEXIST: ErrorCode.E_INVALID_INPUT,
116
- EINVAL: ErrorCode.E_INVALID_INPUT,
104
+ ENOENT: ErrorCode.NOT_FOUND,
105
+ EACCES: ErrorCode.PERMISSION_DENIED,
106
+ EPERM: ErrorCode.PERMISSION_DENIED,
107
+ ENOTDIR: ErrorCode.NOT_DIRECTORY,
108
+ EISDIR: ErrorCode.NOT_FILE,
109
+ ELOOP: ErrorCode.SYMLINK_NOT_ALLOWED,
110
+ ENAMETOOLONG: ErrorCode.INVALID_INPUT,
111
+ ETIMEDOUT: ErrorCode.TIMEOUT,
112
+ EMFILE: ErrorCode.TIMEOUT,
113
+ ENFILE: ErrorCode.TIMEOUT,
114
+ EBUSY: ErrorCode.PERMISSION_DENIED,
115
+ ENOTEMPTY: ErrorCode.NOT_DIRECTORY,
116
+ EEXIST: ErrorCode.INVALID_INPUT,
117
+ EINVAL: ErrorCode.INVALID_INPUT,
117
118
  };
118
119
  function isKnownNodeErrorCode(code) {
119
120
  return code in NODE_ERROR_CODE_MAP;
@@ -169,24 +170,39 @@ export class McpError extends Error {
169
170
  super(message, { cause });
170
171
  this.code = code;
171
172
  this.path = path;
172
- this.details = details;
173
173
  this.name = 'McpError';
174
174
  Object.setPrototypeOf(this, McpError.prototype);
175
+ const trace = getTraceContext();
176
+ if (trace?.traceparent || details) {
177
+ this.details = { ...trace, ...details };
178
+ }
179
+ }
180
+ static notFound(message, path, details, cause) {
181
+ return new McpError(ErrorCode.NOT_FOUND, message, path, details, cause);
182
+ }
183
+ static invalidInput(message, path, details, cause) {
184
+ return new McpError(ErrorCode.INVALID_INPUT, message, path, details, cause);
185
+ }
186
+ static accessDenied(message, path, details, cause) {
187
+ return new McpError(ErrorCode.ACCESS_DENIED, message, path, details, cause);
188
+ }
189
+ static timeout(message, path, details, cause) {
190
+ return new McpError(ErrorCode.TIMEOUT, message, path, details, cause);
175
191
  }
176
192
  }
177
193
  const ERROR_SUGGESTIONS = {
178
- [ErrorCode.E_ACCESS_DENIED]: 'Check that the path is within an allowed directory. Use roots to see available workspace roots.',
179
- [ErrorCode.E_NOT_FOUND]: 'Verify the path exists. Use ls to explore available files and directories.',
180
- [ErrorCode.E_NOT_FILE]: 'The path points to a directory or other non-file. Use ls to explore its contents.',
181
- [ErrorCode.E_NOT_DIRECTORY]: 'The path points to a file, not a directory. Use read to read file contents.',
182
- [ErrorCode.E_TOO_LARGE]: 'The file exceeds the size limit. Use head to read a partial preview, or narrow the scope of what you read.',
183
- [ErrorCode.E_TIMEOUT]: 'The operation timed out. Try a smaller scope (narrower path), fewer results (maxResults), or search fewer files.',
184
- [ErrorCode.E_CANCELLED]: 'The operation was cancelled. This is not an error — no retry is needed unless you want to re-run the operation.',
185
- [ErrorCode.E_INVALID_PATTERN]: 'The glob or regex pattern is invalid. Check syntax and escape special characters.',
186
- [ErrorCode.E_INVALID_INPUT]: 'One or more input parameters are invalid. Check the tool documentation for correct usage.',
187
- [ErrorCode.E_PERMISSION_DENIED]: 'Permission denied by the operating system. Check file permissions.',
188
- [ErrorCode.E_SYMLINK_NOT_ALLOWED]: 'Symbolic links that escape allowed directories are not permitted for security reasons.',
189
- [ErrorCode.E_UNKNOWN]: 'An unexpected error occurred. Check the error message for details.',
194
+ [ErrorCode.ACCESS_DENIED]: 'Run roots to list allowed directories.',
195
+ [ErrorCode.NOT_FOUND]: 'Run ls or find to verify the path.',
196
+ [ErrorCode.NOT_FILE]: 'Target is a directory, not a file.',
197
+ [ErrorCode.NOT_DIRECTORY]: 'Target is a file, not a directory.',
198
+ [ErrorCode.TOO_LARGE]: 'Use head/tail or line ranges to read partially.',
199
+ [ErrorCode.TIMEOUT]: 'Reduce scope, depth, or maxResults.',
200
+ [ErrorCode.CANCELLED]: undefined,
201
+ [ErrorCode.INVALID_PATTERN]: 'Check syntax and escape special characters.',
202
+ [ErrorCode.INVALID_INPUT]: undefined,
203
+ [ErrorCode.PERMISSION_DENIED]: 'Check OS file permissions.',
204
+ [ErrorCode.SYMLINK_NOT_ALLOWED]: 'Symlink escapes allowed directories.',
205
+ [ErrorCode.UNKNOWN]: undefined,
190
206
  };
191
207
  const NOT_FOUND_PATTERNS = [
192
208
  'no such file or directory',
@@ -207,16 +223,16 @@ function classifyMessageError(error) {
207
223
  const message = isNativeError(error) ? error.message : String(error);
208
224
  const lower = message.toLowerCase();
209
225
  if (messageIncludesAny(lower, NOT_FOUND_PATTERNS)) {
210
- return ErrorCode.E_NOT_FOUND;
226
+ return ErrorCode.NOT_FOUND;
211
227
  }
212
228
  if (messageIncludesAny(lower, PERMISSION_DENIED_PATTERNS)) {
213
- return ErrorCode.E_PERMISSION_DENIED;
229
+ return ErrorCode.PERMISSION_DENIED;
214
230
  }
215
231
  if (lower.includes('not a directory')) {
216
- return ErrorCode.E_NOT_DIRECTORY;
232
+ return ErrorCode.NOT_DIRECTORY;
217
233
  }
218
234
  if (lower.includes('is a directory')) {
219
- return ErrorCode.E_NOT_FILE;
235
+ return ErrorCode.NOT_FILE;
220
236
  }
221
237
  return undefined;
222
238
  }
@@ -225,16 +241,16 @@ function classifyError(error) {
225
241
  let fallbackCode;
226
242
  const terminalCode = walkErrorChain(error, (candidate) => {
227
243
  if (isAbortErrorSingle(candidate)) {
228
- return ErrorCode.E_CANCELLED;
244
+ return ErrorCode.CANCELLED;
229
245
  }
230
246
  if (timeoutCode === undefined && isTimeoutErrorSingle(candidate)) {
231
- timeoutCode = ErrorCode.E_TIMEOUT;
247
+ timeoutCode = ErrorCode.TIMEOUT;
232
248
  }
233
249
  fallbackCode ??=
234
250
  getDirectErrorCode(candidate) ?? classifyMessageError(candidate);
235
251
  return undefined;
236
252
  });
237
- return terminalCode ?? timeoutCode ?? fallbackCode ?? ErrorCode.E_UNKNOWN;
253
+ return terminalCode ?? timeoutCode ?? fallbackCode ?? ErrorCode.UNKNOWN;
238
254
  }
239
255
  export function createDetailedError(error, path, additionalDetails) {
240
256
  const message = formatUnknownErrorMessage(error);
@@ -242,7 +258,11 @@ export function createDetailedError(error, path, additionalDetails) {
242
258
  const suggestion = ERROR_SUGGESTIONS[code];
243
259
  const resolvedPath = resolveErrorPath(error, path);
244
260
  const details = mergeErrorDetails(error, additionalDetails);
245
- const result = { code, message, suggestion };
261
+ const result = {
262
+ code,
263
+ message,
264
+ ...(suggestion ? { suggestion } : {}),
265
+ };
246
266
  if (resolvedPath)
247
267
  result.path = resolvedPath;
248
268
  if (details)
@@ -267,12 +287,12 @@ function mergeErrorDetails(error, additionalDetails) {
267
287
  return mergedDetails;
268
288
  }
269
289
  export function formatDetailedError(error) {
270
- const lines = [`Error [${error.code}]: ${error.message}`];
271
- if (error.path) {
272
- lines.push(`Path: ${error.path}`);
290
+ const lines = [`${error.code}: ${error.message}`];
291
+ if (error.path && !error.message.includes(error.path)) {
292
+ lines.push(error.path);
273
293
  }
274
294
  if (error.suggestion) {
275
- lines.push(`Suggestion: ${error.suggestion}`);
295
+ lines.push(error.suggestion);
276
296
  }
277
297
  return joinLines(lines);
278
298
  }
@@ -1,8 +1,8 @@
1
1
  import { type Ignore } from 'ignore';
2
2
  export declare function needsStatsForSort(sortBy: string): boolean;
3
- export declare function withOptionalStoppedReason<T extends object, R extends string>(summary: T, stoppedReason: R | undefined): T | (T & {
4
- stoppedReason: R;
5
- });
3
+ export declare function withOptionalStoppedReason<T extends object, R extends string>(summary: T, stoppedReason: R | undefined): T & {
4
+ stoppedReason?: R;
5
+ };
6
6
  export interface DirentLike {
7
7
  isDirectory(): boolean;
8
8
  isFile(): boolean;
@@ -1,5 +1,5 @@
1
- import * as fs from 'node:fs/promises';
2
- import * as path from 'node:path';
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join, relative } from 'node:path';
3
3
  import ignore, {} from 'ignore';
4
4
  import { isNodeError } from '../errors.js';
5
5
  import { toPosixPath } from '../paths.js';
@@ -40,7 +40,8 @@ export function compareOptionalNumberDesc(left, right, tieBreak) {
40
40
  }
41
41
  export function stableSortByDerivedString(items, derive, tieBreak) {
42
42
  const decorated = [];
43
- for (let index = 0; index < items.length; index += 1) {
43
+ const len = items.length;
44
+ for (let index = 0; index < len; index++) {
44
45
  const item = items[index];
45
46
  if (item === undefined)
46
47
  continue;
@@ -59,7 +60,8 @@ export function stableSortByDerivedString(items, derive, tieBreak) {
59
60
  return tiedCompare;
60
61
  return left.index - right.index;
61
62
  });
62
- for (let index = 0; index < decorated.length; index += 1) {
63
+ const decoratedLen = decorated.length;
64
+ for (let index = 0; index < decoratedLen; index++) {
63
65
  const entry = decorated[index];
64
66
  if (!entry)
65
67
  continue;
@@ -74,13 +76,14 @@ export function applyIndexedValues(output, results) {
74
76
  }
75
77
  }
76
78
  export function applyIndexedErrors(options) {
77
- for (const failure of options.errors) {
78
- const resolvedIndex = options.resolveIndex(failure.index);
79
+ const { output, errors, resolveIndex, buildValue } = options;
80
+ for (const failure of errors) {
81
+ const resolvedIndex = resolveIndex(failure.index);
79
82
  if (resolvedIndex === undefined)
80
83
  continue;
81
- if (resolvedIndex < 0 || resolvedIndex >= options.output.length)
84
+ if (resolvedIndex < 0 || resolvedIndex >= output.length)
82
85
  continue;
83
- options.output[resolvedIndex] = options.buildValue(resolvedIndex, failure.error);
86
+ output[resolvedIndex] = buildValue(resolvedIndex, failure.error);
84
87
  }
85
88
  }
86
89
  export async function isEntryAccessibleByType(entryPath, entryType, rootDirectories, signal, deps) {
@@ -101,8 +104,9 @@ export async function isEntryAccessibleByType(entryPath, entryType, rootDirector
101
104
  }
102
105
  function parseGitignoreLines(contents) {
103
106
  const lines = [];
104
- for (const line of contents.split(/\r?\n/u)) {
105
- const trimmed = line.trim();
107
+ const parts = contents.split(/\r?\n/u);
108
+ for (const part of parts) {
109
+ const trimmed = part.trim();
106
110
  if (trimmed.length > 0) {
107
111
  lines.push(trimmed);
108
112
  }
@@ -110,13 +114,15 @@ function parseGitignoreLines(contents) {
110
114
  return lines;
111
115
  }
112
116
  export async function loadRootGitignore(root, signal) {
113
- const gitignorePath = path.join(root, '.gitignore');
114
- let contents;
117
+ const gitignorePath = join(root, '.gitignore');
115
118
  try {
116
- contents = await fs.readFile(gitignorePath, {
119
+ const contents = await readFile(gitignorePath, {
117
120
  encoding: 'utf-8',
118
121
  signal,
119
122
  });
123
+ const matcher = ignore();
124
+ matcher.add(parseGitignoreLines(contents));
125
+ return matcher;
120
126
  }
121
127
  catch (error) {
122
128
  if (isNodeError(error) && error.code === 'ENOENT') {
@@ -124,16 +130,13 @@ export async function loadRootGitignore(root, signal) {
124
130
  }
125
131
  throw error;
126
132
  }
127
- const matcher = ignore();
128
- matcher.add(parseGitignoreLines(contents));
129
- return matcher;
130
133
  }
131
134
  export function isIgnoredByGitignore(matcher, root, absolutePath, options = {}) {
132
- let relative = options.relativePath;
133
- relative ??= path.relative(root, absolutePath);
134
- if (relative.length === 0)
135
+ let { relativePath } = options;
136
+ relativePath ??= relative(root, absolutePath);
137
+ if (relativePath.length === 0)
135
138
  return false;
136
- const normalized = toPosixPath(relative);
139
+ const normalized = toPosixPath(relativePath);
137
140
  if (options.isDirectory) {
138
141
  return matcher.ignores(normalized.endsWith('/') ? normalized : `${normalized}/`);
139
142
  }
@@ -1,5 +1,5 @@
1
1
  import type { FileInfo, GetMultipleFileInfoResult, ListDirectoryResult } from '../../config.js';
2
- import type { EntryType } from './core.js';
2
+ import { type EntryType } from './core.js';
3
3
  interface FileInfoOptions {
4
4
  includeMimeType?: boolean | undefined;
5
5
  signal?: AbortSignal | undefined;
@@ -60,7 +60,7 @@ interface ReadMultipleResult {
60
60
  endLine?: number;
61
61
  linesRead?: number;
62
62
  hasMoreLines?: boolean;
63
- error?: string;
63
+ error?: Error;
64
64
  }
65
65
  interface ReadMultipleOptions {
66
66
  encoding?: BufferEncoding;