@j0hanz/filesystem-mcp 1.13.2 → 1.14.1

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 (74) hide show
  1. package/README.md +162 -145
  2. package/dist/cli.js +2 -2
  3. package/dist/completions.js +54 -51
  4. package/dist/config.d.ts +13 -14
  5. package/dist/config.js +12 -12
  6. package/dist/index.js +1 -1
  7. package/dist/lib/abort.d.ts +7 -0
  8. package/dist/lib/abort.js +81 -0
  9. package/dist/lib/constants.d.ts +3 -1
  10. package/dist/lib/constants.js +8 -2
  11. package/dist/lib/errors.d.ts +7 -3
  12. package/dist/lib/errors.js +64 -41
  13. package/dist/lib/file-operations/core.d.ts +3 -3
  14. package/dist/lib/file-operations/core.js +23 -20
  15. package/dist/lib/file-operations/metadata.d.ts +2 -2
  16. package/dist/lib/file-operations/metadata.js +69 -22
  17. package/dist/lib/file-operations/search.d.ts +0 -1
  18. package/dist/lib/file-operations/search.js +87 -95
  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 +29 -108
  22. package/dist/lib/globs.d.ts +2 -0
  23. package/dist/lib/globs.js +19 -0
  24. package/dist/lib/logger.d.ts +28 -0
  25. package/dist/lib/logger.js +91 -0
  26. package/dist/lib/observability.d.ts +7 -0
  27. package/dist/lib/observability.js +19 -9
  28. package/dist/lib/paths.js +55 -55
  29. package/dist/lib/resource-store.js +4 -4
  30. package/dist/lib/utils.d.ts +0 -12
  31. package/dist/lib/utils.js +0 -13
  32. package/dist/lib/zod-codecs.d.ts +2 -0
  33. package/dist/lib/zod-codecs.js +18 -0
  34. package/dist/pkg-info.d.ts +1 -0
  35. package/dist/pkg-info.js +2 -2
  36. package/dist/prompts.js +3 -3
  37. package/dist/resources/generated-instructions.js +41 -41
  38. package/dist/resources/tool-catalog.js +33 -58
  39. package/dist/resources/tool-info.d.ts +0 -1
  40. package/dist/resources/tool-info.js +44 -67
  41. package/dist/resources/workflows.js +47 -19
  42. package/dist/resources.d.ts +1 -1
  43. package/dist/resources.js +4 -4
  44. package/dist/schemas.d.ts +185 -465
  45. package/dist/schemas.js +174 -206
  46. package/dist/server/bootstrap.d.ts +12 -11
  47. package/dist/server/bootstrap.js +95 -86
  48. package/dist/server/roots-manager.d.ts +5 -2
  49. package/dist/server/roots-manager.js +9 -7
  50. package/dist/server/task-store.d.ts +10 -0
  51. package/dist/server/task-store.js +73 -0
  52. package/dist/tools/apply-patch.js +39 -20
  53. package/dist/tools/calculate-hash.js +14 -27
  54. package/dist/tools/create-directory.js +11 -9
  55. package/dist/tools/delete-file.js +19 -19
  56. package/dist/tools/diff-files.js +16 -18
  57. package/dist/tools/edit-file.js +11 -5
  58. package/dist/tools/list-directory.js +16 -21
  59. package/dist/tools/move-file.js +105 -100
  60. package/dist/tools/read-multiple.js +15 -10
  61. package/dist/tools/read.js +6 -7
  62. package/dist/tools/replace-in-files.js +76 -115
  63. package/dist/tools/roots.js +3 -7
  64. package/dist/tools/search-content.js +158 -203
  65. package/dist/tools/search-files.js +59 -50
  66. package/dist/tools/shared.d.ts +10 -0
  67. package/dist/tools/shared.js +105 -36
  68. package/dist/tools/stat-many.js +15 -9
  69. package/dist/tools/stat.js +6 -6
  70. package/dist/tools/task-support.d.ts +10 -9
  71. package/dist/tools/task-support.js +94 -23
  72. package/dist/tools/tree.js +4 -4
  73. package/dist/tools/write-file.js +11 -12
  74. package/package.json +10 -9
@@ -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;
@@ -1,11 +1,53 @@
1
- import * as fsp from 'node:fs/promises';
2
- import * as path from 'node:path';
1
+ import { lstat, readdir, readlink, stat, } from 'node:fs';
2
+ import { basename, join, parse, relative } from 'node:path';
3
+ import { assertNotAborted, withAbort, withTimedAbortSignal } from '../abort.js';
3
4
  import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_LIST_MAX_ENTRIES, DEFAULT_MAX_DEPTH, DEFAULT_READ_MANY_MAX_TOTAL_SIZE, DEFAULT_SEARCH_TIMEOUT_MS, getMimeType, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../constants.js';
4
5
  import { isAbortError } from '../errors.js';
5
- import { assertNotAborted, getFileType, isHidden, processInParallel, readFile, readFileWithStats, withAbort, withTimedAbortSignal, } from '../fs-helpers.js';
6
+ import { getFileType, isHidden, processInParallel, readFile, readFileWithStats, } from '../fs-helpers.js';
7
+ import { assertSafeGlobPattern } from '../globs.js';
6
8
  import { assertAllowedFileAccess, isPathWithinDirectories, isSensitivePath, normalizePath, toPosixPath, validateExistingDirectory, validateExistingPath, validateExistingPathDetailed, } from '../paths.js';
7
9
  import { applyIndexedErrors, applyIndexedValues, isEntryAccessibleByType, isIgnoredByGitignore, loadRootGitignore, needsStatsForSort, resolveEntryType, resolveStopReason, withOptionalStoppedReason, } from './core.js';
8
10
  import { globEntries } from './traversal.js';
11
+ function statAsync(filePath) {
12
+ return new Promise((resolve, reject) => {
13
+ stat(filePath, (err, stats) => {
14
+ if (err)
15
+ reject(err);
16
+ else
17
+ resolve(stats);
18
+ });
19
+ });
20
+ }
21
+ function readlinkAsync(filePath) {
22
+ return new Promise((resolve, reject) => {
23
+ readlink(filePath, (err, linkString) => {
24
+ if (err)
25
+ reject(err);
26
+ else
27
+ resolve(linkString);
28
+ });
29
+ });
30
+ }
31
+ function readdirAsync(dirPath, options) {
32
+ return new Promise((resolve, reject) => {
33
+ readdir(dirPath, options, (err, files) => {
34
+ if (err)
35
+ reject(err);
36
+ else
37
+ resolve(files);
38
+ });
39
+ });
40
+ }
41
+ function lstatAsync(filePath) {
42
+ return new Promise((resolve, reject) => {
43
+ lstat(filePath, (err, stats) => {
44
+ if (err)
45
+ reject(err);
46
+ else
47
+ resolve(stats);
48
+ });
49
+ });
50
+ }
9
51
  const ACCESS_DEPS = {
10
52
  normalizePath,
11
53
  isPathWithinDirectories,
@@ -48,7 +90,7 @@ function buildFileInfoResult(name, requestedPath, isSymlink, stats, mimeType, sy
48
90
  async function getSymlinkTarget(pathToRead, signal) {
49
91
  assertNotAborted(signal);
50
92
  try {
51
- return await withAbort(fsp.readlink(pathToRead), signal);
93
+ return await withAbort(readlinkAsync(pathToRead), signal);
52
94
  }
53
95
  catch (error) {
54
96
  if (isAbortError(error))
@@ -61,14 +103,14 @@ export async function getFileInfo(filePath, options = {}) {
61
103
  assertNotAborted(signal);
62
104
  const { requestedPath, resolvedPath, isSymlink } = await validateExistingPathDetailed(filePath, signal);
63
105
  assertAllowedFileAccess(requestedPath, resolvedPath);
64
- const { base: name, ext: rawExt } = path.parse(requestedPath);
106
+ const { base: name, ext: rawExt } = parse(requestedPath);
65
107
  const ext = rawExt.toLowerCase();
66
108
  const includeMimeType = options.includeMimeType !== false;
67
109
  const mimeType = includeMimeType && ext.length > 0 ? getMimeType(ext) : undefined;
68
110
  const symlinkTarget = isSymlink
69
111
  ? await getSymlinkTarget(requestedPath, signal)
70
112
  : undefined;
71
- const stats = await withAbort(fsp.stat(resolvedPath), signal);
113
+ const stats = await withAbort(statAsync(resolvedPath), signal);
72
114
  return buildFileInfoResult(name, requestedPath, isSymlink, stats, mimeType, symlinkTarget);
73
115
  }
74
116
  function buildEmptyResult() {
@@ -130,7 +172,7 @@ export async function getMultipleFileInfo(paths, options = {}) {
130
172
  : undefined,
131
173
  buildValue: (resolvedIndex, error) => ({
132
174
  path: paths[resolvedIndex] ?? UNKNOWN_PATH,
133
- error: error.message,
175
+ error,
134
176
  }),
135
177
  });
136
178
  return {
@@ -149,6 +191,7 @@ function normalizeListOptions(options) {
149
191
  timeoutMs: options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
150
192
  };
151
193
  if (options.pattern && options.pattern.length > 0) {
194
+ assertSafeGlobPattern(options.pattern);
152
195
  normalized.pattern = options.pattern;
153
196
  }
154
197
  return normalized;
@@ -166,12 +209,12 @@ function resolveMaxDepth(normalized) {
166
209
  return normalized.pattern ? normalized.maxDepth : 1;
167
210
  }
168
211
  async function* readDirectoryEntries(basePath, normalized, needsStats, signal) {
169
- const dirents = await withAbort(fsp.readdir(basePath, { withFileTypes: true }), signal);
212
+ const dirents = await withAbort(readdirAsync(basePath, { withFileTypes: true }), signal);
170
213
  if (!needsStats) {
171
214
  for (const dirent of dirents) {
172
215
  if (!normalized.includeHidden && isHidden(dirent.name))
173
216
  continue;
174
- yield { path: path.join(basePath, dirent.name), dirent };
217
+ yield { path: join(basePath, dirent.name), dirent };
175
218
  }
176
219
  return;
177
220
  }
@@ -179,12 +222,12 @@ async function* readDirectoryEntries(basePath, normalized, needsStats, signal) {
179
222
  for (const dirent of dirents) {
180
223
  if (!normalized.includeHidden && isHidden(dirent.name))
181
224
  continue;
182
- filtered.push({ dirent, entryPath: path.join(basePath, dirent.name) });
225
+ filtered.push({ dirent, entryPath: join(basePath, dirent.name) });
183
226
  }
184
227
  const { results, errors } = await processInParallel(filtered, async ({ entryPath, dirent }) => ({
185
228
  path: entryPath,
186
229
  dirent,
187
- stats: await withAbort(fsp.lstat(entryPath), signal),
230
+ stats: await withAbort(lstatAsync(entryPath), signal),
188
231
  }), PARALLEL_CONCURRENCY, signal);
189
232
  if (errors.length > 0) {
190
233
  throw errors[0]?.error ?? new Error('Failed to read entry stats');
@@ -214,13 +257,13 @@ function shouldUseFastPath(normalized, maxDepth) {
214
257
  maxDepth === 1);
215
258
  }
216
259
  function resolveRelativePath(basePath, entryPath) {
217
- return path.relative(basePath, entryPath) || path.basename(entryPath);
260
+ return relative(basePath, entryPath) || basename(entryPath);
218
261
  }
219
262
  async function resolveSymlinkTarget(entryType, includeSymlinkTargets, entryPath) {
220
263
  if (entryType !== 'symlink' || !includeSymlinkTargets) {
221
264
  return undefined;
222
265
  }
223
- return fsp.readlink(entryPath).catch(() => undefined);
266
+ return readlinkAsync(entryPath).catch(() => undefined);
224
267
  }
225
268
  function updateTotals(entryType, totals) {
226
269
  if (entryType === 'file')
@@ -232,7 +275,7 @@ function buildDirectoryEntry(basePath, entry, entryType, needsStats, symlinkTarg
232
275
  const size = needsStats && entry.stats?.isFile() ? entry.stats.size : undefined;
233
276
  const modified = needsStats ? entry.stats?.mtime : undefined;
234
277
  return {
235
- name: path.basename(entry.path),
278
+ name: basename(entry.path),
236
279
  path: entry.path,
237
280
  relativePath: resolveRelativePath(basePath, entry.path),
238
281
  type: entryType,
@@ -421,7 +464,7 @@ async function resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher,
421
464
  return null;
422
465
  }
423
466
  const relativePosix = toPosixPath(resolveRelativePath(root, entry.path));
424
- const name = path.basename(entry.path);
467
+ const name = basename(entry.path);
425
468
  return { type, relativePosix, name };
426
469
  }
427
470
  function upsertChildNode(parent, nodeByPath, resolved, childPathIndexByParent) {
@@ -521,7 +564,7 @@ export async function treeDirectory(dirPath, options = {}) {
521
564
  ? null
522
565
  : await loadRootGitignore(root, signal);
523
566
  const rootNode = {
524
- name: path.basename(root) || root,
567
+ name: basename(root) || root,
525
568
  type: 'directory',
526
569
  relativePath: '.',
527
570
  children: [],
@@ -590,6 +633,7 @@ function buildReadOptions(options) {
590
633
  const readOptions = {
591
634
  encoding: options.encoding,
592
635
  maxSize: options.maxSize,
636
+ skipBinary: true,
593
637
  };
594
638
  applyLineSelection(readOptions, options);
595
639
  return readOptions;
@@ -653,10 +697,13 @@ function applyLineSelection(target, source) {
653
697
  target.head = source.head;
654
698
  if (source.tail !== undefined)
655
699
  target.tail = source.tail;
656
- if (source.startLine !== undefined)
657
- target.startLine = source.startLine;
658
- if (source.endLine !== undefined)
700
+ if (source.endLine !== undefined) {
701
+ target.startLine = source.startLine ?? 1;
659
702
  target.endLine = source.endLine;
703
+ }
704
+ else if (source.startLine !== undefined) {
705
+ target.startLine = source.startLine;
706
+ }
660
707
  }
661
708
  function resolveNormalizedReadOptions(options) {
662
709
  const { signal, ...rest } = options;
@@ -669,7 +716,7 @@ function resolveNormalizedReadOptions(options) {
669
716
  }
670
717
  async function validateFile(filePath, index, signal) {
671
718
  const validPath = await validateExistingPath(filePath, signal);
672
- const stats = await withAbort(fsp.stat(validPath), signal);
719
+ const stats = await withAbort(statAsync(validPath), signal);
673
720
  return { filePath, index, validPath, stats };
674
721
  }
675
722
  function markRemainingSkipped(startIndex, total, skippedBudget) {
@@ -804,7 +851,7 @@ function applySkippedBudget(output, skippedBudget, filePaths, maxTotalSize) {
804
851
  continue;
805
852
  output[index] = {
806
853
  path: filePath,
807
- error: `Skipped: combined estimated read would exceed maxTotalSize (${maxTotalSize} bytes)`,
854
+ error: new Error(`Skipped: combined estimated read would exceed maxTotalSize (${maxTotalSize} bytes)`),
808
855
  };
809
856
  }
810
857
  }
@@ -823,7 +870,7 @@ export async function readMultipleFiles(filePaths, options = {}) {
823
870
  resolveIndex: (failureIndex) => resolveErrorOriginalIndex(failureIndex, filesToProcess, filePaths.length),
824
871
  buildValue: (resolvedIndex, error) => ({
825
872
  path: filePaths[resolvedIndex] ?? UNKNOWN_PATH,
826
- error: error.message,
873
+ error,
827
874
  }),
828
875
  });
829
876
  applySkippedBudget(output, skippedBudget, filePaths, normalized.maxTotalSize);
@@ -12,7 +12,6 @@ declare const SearchOptionsSchema: z.ZodObject<{
12
12
  contextLines: z.ZodInt;
13
13
  wholeWord: z.ZodBoolean;
14
14
  isLiteral: z.ZodBoolean;
15
- multiline: z.ZodBoolean;
16
15
  includeHidden: z.ZodBoolean;
17
16
  baseNameMatch: z.ZodBoolean;
18
17
  caseSensitiveFileMatch: z.ZodBoolean;