@j0hanz/filesystem-mcp 1.8.0 → 1.9.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.
- package/README.md +635 -377
- package/dist/cli.js +2 -2
- package/dist/completions.js +2 -3
- package/dist/index.js +2 -2
- package/dist/lib/file-operations/{common.d.ts → core.d.ts} +6 -0
- package/dist/lib/file-operations/{common.js → core.js} +45 -0
- package/dist/lib/file-operations/metadata.d.ts +73 -0
- package/dist/lib/file-operations/metadata.js +889 -0
- package/dist/lib/file-operations/{search-content.d.ts → search.d.ts} +40 -7
- package/dist/lib/file-operations/{search-content.js → search.js} +418 -20
- package/dist/lib/file-operations/{glob-engine.d.ts → traversal.d.ts} +18 -1
- package/dist/lib/file-operations/{glob-engine.js → traversal.js} +25 -2
- package/dist/lib/fs-helpers.js +1 -2
- package/dist/lib/observability.js +1 -1
- package/dist/lib/{path-validation.d.ts → paths.d.ts} +11 -0
- package/dist/lib/{path-validation.js → paths.js} +145 -12
- package/dist/lib/utils.d.ts +15 -0
- package/dist/lib/utils.js +33 -0
- package/dist/resources/generated-instructions.js +6 -6
- package/dist/resources/tool-catalog.js +9 -9
- package/dist/resources/tool-info.js +3 -3
- package/dist/resources/workflows.js +10 -23
- package/dist/schemas.d.ts +11 -11
- package/dist/schemas.js +18 -31
- package/dist/server/bootstrap.d.ts +19 -1
- package/dist/server/bootstrap.js +130 -44
- package/dist/server/roots-manager.d.ts +5 -2
- package/dist/server/roots-manager.js +17 -5
- package/dist/tools/apply-patch.js +4 -3
- package/dist/tools/calculate-hash.js +4 -4
- package/dist/tools/create-directory.js +1 -1
- package/dist/tools/delete-file.js +3 -3
- package/dist/tools/diff-files.js +2 -2
- package/dist/tools/edit-file.js +14 -13
- package/dist/tools/list-directory.js +5 -7
- package/dist/tools/move-file.js +2 -1
- package/dist/tools/read-multiple.js +3 -4
- package/dist/tools/read.js +3 -3
- package/dist/tools/replace-in-files.js +8 -10
- package/dist/tools/roots.js +4 -6
- package/dist/tools/search-content.js +7 -9
- package/dist/tools/search-files.js +4 -6
- package/dist/tools/shared.d.ts +2 -1
- package/dist/tools/shared.js +2 -1
- package/dist/tools/stat-many.js +4 -3
- package/dist/tools/stat.js +4 -3
- package/dist/tools/task-support.js +1 -1
- package/dist/tools/tree.js +3 -4
- package/dist/tools/write-file.js +1 -1
- package/package.json +4 -4
- package/dist/lib/file-operations/file-info.d.ts +0 -10
- package/dist/lib/file-operations/file-info.js +0 -143
- package/dist/lib/file-operations/gitignore.d.ts +0 -6
- package/dist/lib/file-operations/gitignore.js +0 -45
- package/dist/lib/file-operations/glob-helpers.d.ts +0 -18
- package/dist/lib/file-operations/glob-helpers.js +0 -23
- package/dist/lib/file-operations/list-directory.d.ts +0 -14
- package/dist/lib/file-operations/list-directory.js +0 -252
- package/dist/lib/file-operations/read-multiple-files.d.ts +0 -25
- package/dist/lib/file-operations/read-multiple-files.js +0 -252
- package/dist/lib/file-operations/search-files.d.ts +0 -27
- package/dist/lib/file-operations/search-files.js +0 -216
- package/dist/lib/file-operations/search-matcher.d.ts +0 -10
- package/dist/lib/file-operations/search-matcher.js +0 -72
- package/dist/lib/file-operations/search-worker.d.ts +0 -2
- package/dist/lib/file-operations/search-worker.js +0 -131
- package/dist/lib/file-operations/tree.d.ts +0 -28
- package/dist/lib/file-operations/tree.js +0 -265
- package/dist/lib/option-utils.d.ts +0 -3
- package/dist/lib/option-utils.js +0 -15
- package/dist/lib/path-format.d.ts +0 -1
- package/dist/lib/path-format.js +0 -7
- package/dist/lib/path-policy.d.ts +0 -2
- package/dist/lib/path-policy.js +0 -100
- package/dist/lib/progress-reporting.d.ts +0 -11
- package/dist/lib/progress-reporting.js +0 -13
- package/dist/lib/type-guards.d.ts +0 -1
- package/dist/lib/type-guards.js +0 -3
- package/dist/server/capabilities.d.ts +0 -10
- package/dist/server/capabilities.js +0 -48
- package/dist/server/logging.d.ts +0 -7
- package/dist/server/logging.js +0 -41
- package/dist/server/types.d.ts +0 -4
- package/dist/server/types.js +0 -1
|
@@ -1,10 +1,114 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
5
|
+
import { platform } from 'node:os';
|
|
4
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { SENSITIVE_FILE_ALLOWLIST, SENSITIVE_FILE_DENYLIST, } from './constants.js';
|
|
5
8
|
import { ErrorCode, isAbortError, isNodeError, McpError } from './errors.js';
|
|
6
9
|
import { assertNotAborted, withAbort } from './fs-helpers.js';
|
|
7
|
-
const
|
|
10
|
+
const WINDOWS_PATH_SEPARATOR = '\\';
|
|
11
|
+
const POSIX_PATH_SEPARATOR = '/';
|
|
12
|
+
export function toPosixPath(value) {
|
|
13
|
+
return value.includes(WINDOWS_PATH_SEPARATOR)
|
|
14
|
+
? value.replace(/\\/gu, POSIX_PATH_SEPARATOR)
|
|
15
|
+
: value;
|
|
16
|
+
}
|
|
17
|
+
const IS_WINDOWS = platform() === 'win32';
|
|
18
|
+
const WINDOWS_ABSOLUTE_RE = /^[a-z]:\//iu;
|
|
19
|
+
function normalizePathForMatch(input) {
|
|
20
|
+
return toPosixPath(path.normalize(input));
|
|
21
|
+
}
|
|
22
|
+
function normalizeForMatch(input) {
|
|
23
|
+
const normalized = normalizePathForMatch(input);
|
|
24
|
+
return IS_WINDOWS ? normalized.toLowerCase() : normalized;
|
|
25
|
+
}
|
|
26
|
+
function compilePatternGlobs(normalizedPattern) {
|
|
27
|
+
const globs = new Set([normalizedPattern]);
|
|
28
|
+
const isWindowsAbsolute = WINDOWS_ABSOLUTE_RE.test(normalizedPattern);
|
|
29
|
+
if (!normalizedPattern.startsWith('**/') && !isWindowsAbsolute) {
|
|
30
|
+
const withoutRoot = normalizedPattern.replace(/^\/+/u, '');
|
|
31
|
+
if (withoutRoot.length > 0) {
|
|
32
|
+
globs.add(`**/${withoutRoot}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return [...globs];
|
|
36
|
+
}
|
|
37
|
+
function compilePatterns(patterns) {
|
|
38
|
+
const unique = new Set();
|
|
39
|
+
for (const pattern of patterns) {
|
|
40
|
+
const trimmed = pattern.trim();
|
|
41
|
+
if (trimmed.length > 0) {
|
|
42
|
+
unique.add(trimmed);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const compiled = [];
|
|
46
|
+
for (const pattern of unique) {
|
|
47
|
+
const normalized = normalizeForMatch(pattern);
|
|
48
|
+
const matchesPath = normalized.includes('/');
|
|
49
|
+
compiled.push({
|
|
50
|
+
globs: matchesPath ? compilePatternGlobs(normalized) : [normalized],
|
|
51
|
+
matchesPath,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return compiled;
|
|
55
|
+
}
|
|
56
|
+
function toPatternSet(patterns) {
|
|
57
|
+
const pathGlobs = new Set();
|
|
58
|
+
const nameGlobs = new Set();
|
|
59
|
+
for (const pattern of patterns) {
|
|
60
|
+
const target = pattern.matchesPath ? pathGlobs : nameGlobs;
|
|
61
|
+
for (const glob of pattern.globs) {
|
|
62
|
+
target.add(glob);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
pathGlobs: [...pathGlobs],
|
|
67
|
+
nameGlobs: [...nameGlobs],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const DENY_PATTERNS = toPatternSet(compilePatterns(SENSITIVE_FILE_DENYLIST));
|
|
71
|
+
const ALLOW_PATTERNS = toPatternSet(compilePatterns(SENSITIVE_FILE_ALLOWLIST));
|
|
72
|
+
function uniquePair(primary, secondary) {
|
|
73
|
+
if (!secondary || secondary === primary)
|
|
74
|
+
return [primary];
|
|
75
|
+
return [primary, secondary];
|
|
76
|
+
}
|
|
77
|
+
function matchesAnyGlobs(globs, candidates) {
|
|
78
|
+
if (globs.length === 0 || candidates.length === 0)
|
|
79
|
+
return false;
|
|
80
|
+
for (const candidate of candidates) {
|
|
81
|
+
for (const glob of globs) {
|
|
82
|
+
if (path.posix.matchesGlob(candidate, glob))
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
export function isSensitivePath(requestedPath, resolvedPath) {
|
|
89
|
+
if (DENY_PATTERNS.pathGlobs.length === 0 &&
|
|
90
|
+
DENY_PATTERNS.nameGlobs.length === 0) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const normalizedRequested = normalizeForMatch(requestedPath);
|
|
94
|
+
const normalizedResolved = resolvedPath
|
|
95
|
+
? normalizeForMatch(resolvedPath)
|
|
96
|
+
: undefined;
|
|
97
|
+
const pathCandidates = uniquePair(normalizedRequested, normalizedResolved);
|
|
98
|
+
const nameCandidates = uniquePair(path.posix.basename(normalizedRequested), normalizedResolved ? path.posix.basename(normalizedResolved) : undefined);
|
|
99
|
+
if (matchesAnyGlobs(ALLOW_PATTERNS.pathGlobs, pathCandidates) ||
|
|
100
|
+
matchesAnyGlobs(ALLOW_PATTERNS.nameGlobs, nameCandidates)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return (matchesAnyGlobs(DENY_PATTERNS.pathGlobs, pathCandidates) ||
|
|
104
|
+
matchesAnyGlobs(DENY_PATTERNS.nameGlobs, nameCandidates));
|
|
105
|
+
}
|
|
106
|
+
export function assertAllowedFileAccess(requestedPath, resolvedPath) {
|
|
107
|
+
if (!isSensitivePath(requestedPath, resolvedPath))
|
|
108
|
+
return;
|
|
109
|
+
throw new McpError(ErrorCode.E_ACCESS_DENIED, `Access denied: sensitive file blocked by policy (${requestedPath}). ` +
|
|
110
|
+
'Set FS_CONTEXT_ALLOW_SENSITIVE=1 or use FS_CONTEXT_ALLOWLIST to override.', requestedPath);
|
|
111
|
+
}
|
|
8
112
|
const HOMEDIR = os.homedir();
|
|
9
113
|
const PATH_SEPARATOR = path.sep;
|
|
10
114
|
const DRIVE_LETTER_REGEX = /^[A-Za-z]:/;
|
|
@@ -34,9 +138,18 @@ const RESERVED_DEVICE_NAMES = new Set([
|
|
|
34
138
|
'LPT8',
|
|
35
139
|
'LPT9',
|
|
36
140
|
]);
|
|
141
|
+
const allowedDirectoriesContext = new AsyncLocalStorage({
|
|
142
|
+
name: 'filesystem-mcp:allowed-directories',
|
|
143
|
+
});
|
|
37
144
|
function dedupePreserveOrder(items) {
|
|
38
145
|
return [...new Set(items)];
|
|
39
146
|
}
|
|
147
|
+
function cloneAllowedDirectoriesState(state) {
|
|
148
|
+
return {
|
|
149
|
+
primary: [...state.primary],
|
|
150
|
+
expanded: [...state.expanded],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
40
153
|
function expandHome(filepath) {
|
|
41
154
|
if (filepath === '~')
|
|
42
155
|
return HOMEDIR;
|
|
@@ -115,26 +228,41 @@ function normalizeAllowedDirectories(dirs) {
|
|
|
115
228
|
// single MCP session per process, so this is safe. In HTTP mode all HTTP
|
|
116
229
|
// sessions within the same process share one policy — multi-tenant isolation
|
|
117
230
|
// (different roots per session) requires separate server processes.
|
|
118
|
-
let
|
|
119
|
-
|
|
231
|
+
let defaultAllowedDirectoriesState = {
|
|
232
|
+
primary: [],
|
|
233
|
+
expanded: [],
|
|
234
|
+
};
|
|
120
235
|
function setAllowedDirectoriesState(primary, expanded) {
|
|
121
|
-
|
|
122
|
-
|
|
236
|
+
defaultAllowedDirectoriesState = {
|
|
237
|
+
primary: dedupePreserveOrder(primary),
|
|
238
|
+
expanded: dedupePreserveOrder(expanded),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function getActiveAllowedDirectoriesState() {
|
|
242
|
+
return allowedDirectoriesContext.getStore() ?? defaultAllowedDirectoriesState;
|
|
243
|
+
}
|
|
244
|
+
export function withAllowedDirectoriesState(state, run) {
|
|
245
|
+
return allowedDirectoriesContext.run(cloneAllowedDirectoriesState(state), run);
|
|
246
|
+
}
|
|
247
|
+
export function getAllowedDirectoriesState() {
|
|
248
|
+
return cloneAllowedDirectoriesState(getActiveAllowedDirectoriesState());
|
|
249
|
+
}
|
|
250
|
+
export function setAllowedDirectoriesStateResolved(state) {
|
|
251
|
+
setAllowedDirectoriesState(state.primary, state.expanded);
|
|
123
252
|
}
|
|
124
253
|
export function getAllowedDirectories() {
|
|
125
|
-
return [...
|
|
254
|
+
return [...getActiveAllowedDirectoriesState().expanded];
|
|
126
255
|
}
|
|
127
256
|
export function isAllowedDirectoryRoot(normalizedPath) {
|
|
128
|
-
for (const dir of
|
|
257
|
+
for (const dir of getActiveAllowedDirectoriesState().expanded) {
|
|
129
258
|
if (isSamePath(normalizedPath, dir))
|
|
130
259
|
return true;
|
|
131
260
|
}
|
|
132
261
|
return false;
|
|
133
262
|
}
|
|
134
263
|
function getAllowedDirectoriesForRelativeResolution() {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
: allowedDirectoriesExpanded;
|
|
264
|
+
const state = getActiveAllowedDirectoriesState();
|
|
265
|
+
return state.primary.length > 0 ? state.primary : state.expanded;
|
|
138
266
|
}
|
|
139
267
|
function isPathInsideDirectory(normalizedDirectory, normalizedCandidate) {
|
|
140
268
|
const root = normalizeForComparison(normalizedDirectory);
|
|
@@ -183,10 +311,14 @@ async function expandAllowedDirectories(primaryDirs, signal) {
|
|
|
183
311
|
}
|
|
184
312
|
return dedupePreserveOrder(expanded);
|
|
185
313
|
}
|
|
186
|
-
export async function
|
|
314
|
+
export async function resolveAllowedDirectoriesState(dirs, signal) {
|
|
187
315
|
const primary = normalizeAllowedDirectories(dirs);
|
|
188
316
|
const expanded = await expandAllowedDirectories(primary, signal);
|
|
189
|
-
|
|
317
|
+
return { primary, expanded };
|
|
318
|
+
}
|
|
319
|
+
export async function setAllowedDirectoriesResolved(dirs, signal) {
|
|
320
|
+
const state = await resolveAllowedDirectoriesState(dirs, signal);
|
|
321
|
+
setAllowedDirectoriesStateResolved(state);
|
|
190
322
|
}
|
|
191
323
|
function ensureNonEmptyPath(requestedPath) {
|
|
192
324
|
if (!requestedPath || requestedPath.trim().length === 0) {
|
|
@@ -393,6 +525,7 @@ export async function validatePathForWrite(requestedPath, signal) {
|
|
|
393
525
|
allowedDirs,
|
|
394
526
|
details: { normalizedPath: normalizedRequested },
|
|
395
527
|
});
|
|
528
|
+
assertAllowedFileAccess(requestedPath, normalizedRequested);
|
|
396
529
|
let current = normalizedRequested;
|
|
397
530
|
for (;;) {
|
|
398
531
|
try {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
2
|
+
export declare function mergeOptions<T extends object>(defaults: T, overrides: Partial<T>): T;
|
|
3
|
+
export declare function omitOptionKeys<T extends object, K extends keyof T>(input: T, keys: readonly K[]): Omit<T, K>;
|
|
4
|
+
export declare function setIfDefined<T extends object, K extends keyof T>(target: T, key: K, value: T[K] | undefined): void;
|
|
5
|
+
export interface ProgressPayload {
|
|
6
|
+
current: number;
|
|
7
|
+
total?: number;
|
|
8
|
+
}
|
|
9
|
+
export type ProgressCallback = ((progress: ProgressPayload) => void) | undefined;
|
|
10
|
+
export interface PeriodicProgressOptions {
|
|
11
|
+
total?: number;
|
|
12
|
+
throttleModulo?: number;
|
|
13
|
+
force?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function reportPeriodicProgress(onProgress: ProgressCallback, current: number, options?: PeriodicProgressOptions): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// type-guards.ts
|
|
2
|
+
export function isRecord(value) {
|
|
3
|
+
return value !== null && typeof value === 'object';
|
|
4
|
+
}
|
|
5
|
+
// option-utils.ts
|
|
6
|
+
export function mergeOptions(defaults, overrides) {
|
|
7
|
+
return { ...defaults, ...overrides };
|
|
8
|
+
}
|
|
9
|
+
export function omitOptionKeys(input, keys) {
|
|
10
|
+
const output = { ...input };
|
|
11
|
+
for (const key of keys) {
|
|
12
|
+
Reflect.deleteProperty(output, key);
|
|
13
|
+
}
|
|
14
|
+
return output;
|
|
15
|
+
}
|
|
16
|
+
export function setIfDefined(target, key, value) {
|
|
17
|
+
if (value !== undefined) {
|
|
18
|
+
target[key] = value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function reportPeriodicProgress(onProgress, current, options = {}) {
|
|
22
|
+
if (!onProgress || current === 0)
|
|
23
|
+
return;
|
|
24
|
+
const throttleModulo = options.throttleModulo ?? 1;
|
|
25
|
+
const force = options.force ?? false;
|
|
26
|
+
if (!force && throttleModulo > 1 && current % throttleModulo !== 0) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
onProgress({
|
|
30
|
+
current,
|
|
31
|
+
...(options.total !== undefined ? { total: options.total } : {}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -2,7 +2,7 @@ import { buildToolCatalogDetailsOnly } from './tool-catalog.js';
|
|
|
2
2
|
import { buildCoreContextPack, getSharedConstraints, getToolContracts, } from './tool-info.js';
|
|
3
3
|
import { buildWorkflowGuide } from './workflows.js';
|
|
4
4
|
const INSTRUCTIONS_HEADER = `<role>
|
|
5
|
-
Filesystem agent
|
|
5
|
+
Filesystem agent. Scope: allowed roots only. Discover paths before acting — never guess.
|
|
6
6
|
</role>
|
|
7
7
|
|
|
8
8
|
<tools_overview>
|
|
@@ -16,10 +16,10 @@ Filesystem agent for local paths only. Operate inside allowed roots. Discover be
|
|
|
16
16
|
|
|
17
17
|
<resources>
|
|
18
18
|
- \`internal://instructions\`: Full usage reference.
|
|
19
|
-
- \`internal://tool-catalog\`: Tool routing and data
|
|
19
|
+
- \`internal://tool-catalog\`: Tool routing and data flow.
|
|
20
20
|
- \`internal://workflows\`: Standard execution sequences.
|
|
21
|
-
- \`internal://tool-info/{name}\`: Per-tool nuances
|
|
22
|
-
- \`filesystem-mcp://result/{id}\`: Cached large output
|
|
21
|
+
- \`internal://tool-info/{name}\`: Per-tool nuances (e.g. \`internal://tool-info/read\`).
|
|
22
|
+
- \`filesystem-mcp://result/{id}\`: Cached large output — call \`resources/read\` immediately when \`resourceUri\` is returned.
|
|
23
23
|
- \`filesystem-mcp://metrics\`: Per-tool runtime metrics.
|
|
24
24
|
</resources>
|
|
25
25
|
|
|
@@ -35,8 +35,8 @@ ${getSharedConstraints()
|
|
|
35
35
|
</constraints>
|
|
36
36
|
|
|
37
37
|
<error_handling>
|
|
38
|
-
- \`E_ACCESS_DENIED\` => call \`roots\`,
|
|
39
|
-
- \`E_NOT_FOUND\` => call \`ls\` or \`find\`,
|
|
38
|
+
- \`E_ACCESS_DENIED\` => call \`roots\`, use an allowed path.
|
|
39
|
+
- \`E_NOT_FOUND\` => call \`ls\` or \`find\`, verify spelling.
|
|
40
40
|
- \`E_TOO_LARGE\` => use \`head\`, line ranges, or \`read_many\`.
|
|
41
41
|
- \`E_TIMEOUT\` => reduce scope or result limits.
|
|
42
42
|
</error_handling>
|
|
@@ -3,26 +3,26 @@ const CATALOG_GUIDE = `<tool_selection_guide>
|
|
|
3
3
|
## Cross-Tool Data Flow
|
|
4
4
|
|
|
5
5
|
\`\`\`
|
|
6
|
-
find
|
|
7
|
-
diff_files
|
|
6
|
+
find(results[].path) -> grep.paths
|
|
7
|
+
diff_files(patch) -> apply_patch.patch
|
|
8
8
|
\`\`\`
|
|
9
9
|
|
|
10
10
|
## Search Strategy
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
12
|
+
- \`find\`: glob file discovery.
|
|
13
|
+
- \`grep\`: text content search.
|
|
14
|
+
- \`search_and_replace\`: replacement only, not discovery.
|
|
15
15
|
|
|
16
16
|
## Write Strategy
|
|
17
17
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
18
|
+
- \`edit\`: precise first-occurrence replacements.
|
|
19
|
+
- \`write\`: create files or overwrite full contents.
|
|
20
|
+
- \`search_and_replace\`: bulk multi-file replacements.
|
|
21
21
|
|
|
22
22
|
## Patch Management
|
|
23
23
|
|
|
24
24
|
- Generate patches with \`diff_files\` first.
|
|
25
|
-
-
|
|
25
|
+
- Validate with \`apply_patch(dryRun:true)\` before writing.
|
|
26
26
|
- \`apply_patch\` accepts unified diffs only.
|
|
27
27
|
</tool_selection_guide>
|
|
28
28
|
`;
|
|
@@ -37,10 +37,10 @@ export function buildCoreContextPack() {
|
|
|
37
37
|
}
|
|
38
38
|
export function getSharedConstraints() {
|
|
39
39
|
return [
|
|
40
|
-
'Use allowed roots only (
|
|
40
|
+
'Use allowed roots only (from CLI negotiation).',
|
|
41
41
|
'Sensitive paths are denylisted by default.',
|
|
42
|
-
`Limits
|
|
43
|
-
'If
|
|
42
|
+
`Limits enforced: max file size ${Math.floor(MAX_TEXT_FILE_SIZE / 1024 / 1024)}MB; search caps ${MAX_SEARCH_RESULTS} files, ${DEFAULT_SEARCH_CONTENT_RESULTS} content matches.`,
|
|
43
|
+
'If response includes `resourceUri`, call `resources/read` immediately — cached results expire on restart.',
|
|
44
44
|
];
|
|
45
45
|
}
|
|
46
46
|
export function buildToolInfo(name) {
|
|
@@ -1,33 +1,20 @@
|
|
|
1
1
|
export function buildWorkflowGuide() {
|
|
2
2
|
return `<workflows>
|
|
3
|
-
### A: EXPLORE
|
|
4
|
-
|
|
5
|
-
1. \`roots\` (list allowed paths).
|
|
6
|
-
2. \`ls\` (flat view) or \`tree\` (recursive view).
|
|
7
|
-
3. \`stat\` or \`stat_many\` (type and size checks).
|
|
8
|
-
4. \`read\` or \`read_many\` (read content).
|
|
3
|
+
### A: EXPLORE — directory layout or file content
|
|
4
|
+
1. \`roots\` → \`ls\` or \`tree\` → \`stat\`/\`stat_many\` → \`read\`/\`read_many\`.
|
|
9
5
|
> **Strict:** Resolve paths first. Never guess.
|
|
10
6
|
|
|
11
|
-
### B: SEARCH
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
2. \`grep\` (content matches).
|
|
15
|
-
3. \`read\` (verify matched context).
|
|
16
|
-
> **Strict:** Do content search with \`grep\`, not \`find\`.
|
|
7
|
+
### B: SEARCH — files by pattern or content
|
|
8
|
+
1. \`find\` (glob) → \`grep\` (content) → \`read\` (verify).
|
|
9
|
+
> **Strict:** Content search with \`grep\`, not \`find\`.
|
|
17
10
|
|
|
18
|
-
### C: EDIT
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
2. \`search_and_replace\` (bulk replacements).
|
|
22
|
-
3. \`mv\` or \`rm\` (layout changes).
|
|
23
|
-
4. \`mkdir\` (directory creation).
|
|
11
|
+
### C: EDIT — modify files or layout
|
|
12
|
+
1. \`edit\` (targeted replacement) or \`search_and_replace\` (bulk).
|
|
13
|
+
2. \`mv\`/\`rm\` (layout) or \`mkdir\` (create dirs).
|
|
24
14
|
> **Strict:** Confirm destructive ops (\`write\`, \`mv\`, \`rm\`, bulk replace).
|
|
25
15
|
|
|
26
|
-
### D: PATCH
|
|
27
|
-
|
|
28
|
-
1. \`diff_files\` (generate).
|
|
29
|
-
2. \`apply_patch\` (dryRun: true).
|
|
30
|
-
3. \`apply_patch\` (dryRun: false).
|
|
16
|
+
### D: PATCH — apply unified diffs
|
|
17
|
+
1. \`diff_files\` → \`apply_patch(dryRun:true)\` → \`apply_patch\`.
|
|
31
18
|
> **Tip:** Feed \`diff_files\` output directly to \`apply_patch\`.
|
|
32
19
|
</workflows>`;
|
|
33
20
|
}
|
package/dist/schemas.d.ts
CHANGED
|
@@ -34,13 +34,13 @@ export declare const ToolErrorResponseSchema: z.ZodObject<{
|
|
|
34
34
|
}, z.core.$strict>;
|
|
35
35
|
}, z.core.$strict>;
|
|
36
36
|
declare const HeadLinesSchema: z.ZodOptional<z.ZodInt>;
|
|
37
|
-
declare const LineNumberSchema: z.
|
|
37
|
+
declare const LineNumberSchema: z.ZodInt;
|
|
38
38
|
export declare const ListDirectoryInputSchema: z.ZodObject<{
|
|
39
39
|
path: z.ZodOptional<z.ZodString>;
|
|
40
40
|
includeHidden: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
41
41
|
includeIgnored: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
42
|
-
maxDepth: z.ZodOptional<z.
|
|
43
|
-
maxEntries: z.ZodDefault<z.ZodOptional<z.
|
|
42
|
+
maxDepth: z.ZodOptional<z.ZodInt>;
|
|
43
|
+
maxEntries: z.ZodDefault<z.ZodOptional<z.ZodInt>>;
|
|
44
44
|
sortBy: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
45
45
|
name: "name";
|
|
46
46
|
size: "size";
|
|
@@ -55,7 +55,7 @@ export declare const ListAllowedDirectoriesInputSchema: z.ZodObject<{}, z.core.$
|
|
|
55
55
|
export declare const SearchFilesInputSchema: z.ZodObject<{
|
|
56
56
|
path: z.ZodOptional<z.ZodString>;
|
|
57
57
|
pattern: z.ZodString;
|
|
58
|
-
maxResults: z.ZodDefault<z.ZodOptional<z.
|
|
58
|
+
maxResults: z.ZodDefault<z.ZodOptional<z.ZodInt>>;
|
|
59
59
|
includeIgnored: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
60
60
|
includeHidden: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
61
61
|
sortBy: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
@@ -64,13 +64,13 @@ export declare const SearchFilesInputSchema: z.ZodObject<{
|
|
|
64
64
|
path: "path";
|
|
65
65
|
modified: "modified";
|
|
66
66
|
}>>>;
|
|
67
|
-
maxDepth: z.ZodOptional<z.
|
|
67
|
+
maxDepth: z.ZodOptional<z.ZodInt>;
|
|
68
68
|
cursor: z.ZodOptional<z.ZodString>;
|
|
69
69
|
}, z.core.$strict>;
|
|
70
70
|
export declare const TreeInputSchema: z.ZodObject<{
|
|
71
71
|
path: z.ZodOptional<z.ZodString>;
|
|
72
|
-
maxDepth: z.ZodDefault<z.ZodOptional<z.
|
|
73
|
-
maxEntries: z.ZodDefault<z.ZodOptional<z.
|
|
72
|
+
maxDepth: z.ZodDefault<z.ZodOptional<z.ZodInt>>;
|
|
73
|
+
maxEntries: z.ZodDefault<z.ZodOptional<z.ZodInt>>;
|
|
74
74
|
includeHidden: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
75
75
|
includeIgnored: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
76
76
|
}, z.core.$strict>;
|
|
@@ -80,8 +80,8 @@ export declare const SearchContentInputSchema: z.ZodObject<{
|
|
|
80
80
|
isRegex: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
81
81
|
caseSensitive: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
82
82
|
wholeWord: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
83
|
-
contextLines: z.ZodDefault<z.ZodOptional<z.
|
|
84
|
-
maxResults: z.ZodDefault<z.ZodOptional<z.
|
|
83
|
+
contextLines: z.ZodDefault<z.ZodOptional<z.ZodInt>>;
|
|
84
|
+
maxResults: z.ZodDefault<z.ZodOptional<z.ZodInt>>;
|
|
85
85
|
filePattern: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
86
86
|
includeHidden: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
87
87
|
includeIgnored: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
@@ -658,7 +658,7 @@ export declare const CalculateHashOutputSchema: z.ZodObject<{
|
|
|
658
658
|
export declare const DiffFilesInputSchema: z.ZodObject<{
|
|
659
659
|
original: z.ZodString;
|
|
660
660
|
modified: z.ZodString;
|
|
661
|
-
context: z.ZodOptional<z.
|
|
661
|
+
context: z.ZodOptional<z.ZodInt>;
|
|
662
662
|
ignoreWhitespace: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
663
663
|
stripTrailingCr: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
664
664
|
}, z.core.$strict>;
|
|
@@ -691,7 +691,7 @@ export declare const DiffFilesOutputSchema: z.ZodObject<{
|
|
|
691
691
|
export declare const ApplyPatchInputSchema: z.ZodObject<{
|
|
692
692
|
path: z.ZodString;
|
|
693
693
|
patch: z.ZodString;
|
|
694
|
-
fuzzFactor: z.ZodOptional<z.
|
|
694
|
+
fuzzFactor: z.ZodOptional<z.ZodInt>;
|
|
695
695
|
autoConvertLineEndings: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
696
696
|
dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
697
697
|
}, z.core.$strict>;
|
package/dist/schemas.js
CHANGED
|
@@ -16,8 +16,8 @@ function isSafeGlobPattern(value) {
|
|
|
16
16
|
return true;
|
|
17
17
|
}
|
|
18
18
|
const MAX_PATH_LENGTH = 4096;
|
|
19
|
-
const DESC_PATH_ROOT = 'Base directory (default: root). Absolute path required if multiple roots
|
|
20
|
-
const DESC_PATH_REQUIRED = 'Absolute path to file or directory.
|
|
19
|
+
const DESC_PATH_ROOT = 'Base directory (default: root). Absolute path required if multiple roots.';
|
|
20
|
+
const DESC_PATH_REQUIRED = 'Absolute path to file or directory.';
|
|
21
21
|
function defaultFalseBoolean(description) {
|
|
22
22
|
return z.boolean().optional().default(false).describe(description);
|
|
23
23
|
}
|
|
@@ -57,10 +57,7 @@ const HeadLinesSchema = z
|
|
|
57
57
|
.max(100000, 'Max: 100,000')
|
|
58
58
|
.optional()
|
|
59
59
|
.describe('Read first N lines');
|
|
60
|
-
const LineNumberSchema = z
|
|
61
|
-
.number()
|
|
62
|
-
.int({ error: 'Must be integer' })
|
|
63
|
-
.min(1, 'Min: 1');
|
|
60
|
+
const LineNumberSchema = z.int({ error: 'Must be integer' }).min(1, 'Min: 1');
|
|
64
61
|
function addReadRangeIssue(ctx, path, message) {
|
|
65
62
|
ctx.addIssue({
|
|
66
63
|
code: 'custom',
|
|
@@ -115,14 +112,12 @@ export const ListDirectoryInputSchema = z.strictObject({
|
|
|
115
112
|
includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
|
|
116
113
|
includeIgnored: defaultFalseBoolean('Include ignored items (node_modules, .git, etc).'),
|
|
117
114
|
maxDepth: z
|
|
118
|
-
.number()
|
|
119
115
|
.int({ error: 'Must be integer' })
|
|
120
116
|
.min(1, 'Min: 1')
|
|
121
117
|
.max(MAX_TREE_DEPTH, `Max: ${MAX_TREE_DEPTH}`)
|
|
122
118
|
.optional()
|
|
123
119
|
.describe('Max recursion depth when pattern is provided'),
|
|
124
120
|
maxEntries: z
|
|
125
|
-
.number()
|
|
126
121
|
.int({ error: 'Must be integer' })
|
|
127
122
|
.min(1, 'Min: 1')
|
|
128
123
|
.max(MAX_LIST_ENTRIES, `Max: ${MAX_LIST_ENTRIES}`)
|
|
@@ -158,7 +153,6 @@ export const SearchFilesInputSchema = z.strictObject({
|
|
|
158
153
|
})
|
|
159
154
|
.describe('Glob pattern (e.g. "**/*.ts", "src/*.js")'),
|
|
160
155
|
maxResults: z
|
|
161
|
-
.number()
|
|
162
156
|
.int({ error: 'Must be integer' })
|
|
163
157
|
.min(1, 'Min: 1')
|
|
164
158
|
.max(MAX_SEARCH_RESULTS, `Max: ${MAX_SEARCH_RESULTS}`)
|
|
@@ -171,7 +165,6 @@ export const SearchFilesInputSchema = z.strictObject({
|
|
|
171
165
|
.default('path')
|
|
172
166
|
.describe('Sort by path, name, size, or modified'),
|
|
173
167
|
maxDepth: z
|
|
174
|
-
.number()
|
|
175
168
|
.int({ error: 'Must be integer' })
|
|
176
169
|
.min(0, 'Min: 0')
|
|
177
170
|
.max(MAX_SEARCH_DEPTH, `Max: ${MAX_SEARCH_DEPTH}`)
|
|
@@ -185,7 +178,6 @@ export const SearchFilesInputSchema = z.strictObject({
|
|
|
185
178
|
export const TreeInputSchema = z.strictObject({
|
|
186
179
|
path: OptionalPathSchema.describe(DESC_PATH_ROOT),
|
|
187
180
|
maxDepth: z
|
|
188
|
-
.number()
|
|
189
181
|
.int({ error: 'Must be integer' })
|
|
190
182
|
.min(0, 'Min: 0')
|
|
191
183
|
.max(MAX_TREE_DEPTH, `Max: ${MAX_TREE_DEPTH}`)
|
|
@@ -193,7 +185,6 @@ export const TreeInputSchema = z.strictObject({
|
|
|
193
185
|
.default(DEFAULT_TREE_DEPTH)
|
|
194
186
|
.describe(`Depth (0=root node only, no children). Default: ${DEFAULT_TREE_DEPTH}`),
|
|
195
187
|
maxEntries: z
|
|
196
|
-
.number()
|
|
197
188
|
.int({ error: 'Must be integer' })
|
|
198
189
|
.min(1, 'Min: 1')
|
|
199
190
|
.max(MAX_TREE_ENTRIES, `Max: ${MAX_TREE_ENTRIES}`)
|
|
@@ -209,12 +200,11 @@ export const SearchContentInputSchema = z.strictObject({
|
|
|
209
200
|
.string()
|
|
210
201
|
.min(1, 'Pattern required')
|
|
211
202
|
.max(1000, 'Max 1000 chars')
|
|
212
|
-
.describe('
|
|
213
|
-
isRegex: defaultFalseBoolean('Treat pattern as
|
|
214
|
-
caseSensitive: defaultFalseBoolean('Case-sensitive matching
|
|
203
|
+
.describe('Search text. RE2 regex when `isRegex=true`.'),
|
|
204
|
+
isRegex: defaultFalseBoolean('Treat pattern as RE2 regex (no lookahead/lookbehind/backrefs).'),
|
|
205
|
+
caseSensitive: defaultFalseBoolean('Case-sensitive matching. Default: case-insensitive.'),
|
|
215
206
|
wholeWord: defaultFalseBoolean('Match whole words only'),
|
|
216
207
|
contextLines: z
|
|
217
|
-
.number()
|
|
218
208
|
.int({ error: 'Must be integer' })
|
|
219
209
|
.min(0, 'Min: 0')
|
|
220
210
|
.max(50, 'Max: 50')
|
|
@@ -222,7 +212,6 @@ export const SearchContentInputSchema = z.strictObject({
|
|
|
222
212
|
.default(0)
|
|
223
213
|
.describe('Include N lines of context before/after matches'),
|
|
224
214
|
maxResults: z
|
|
225
|
-
.number()
|
|
226
215
|
.int({ error: 'Must be integer' })
|
|
227
216
|
.min(0, 'Min: 0')
|
|
228
217
|
.max(MAX_SEARCH_RESULTS, `Max: ${MAX_SEARCH_RESULTS}`)
|
|
@@ -434,7 +423,7 @@ export const CreateDirectoryInputSchema = z
|
|
|
434
423
|
.describe('Absolute paths to directories to create'),
|
|
435
424
|
})
|
|
436
425
|
.refine((data) => data.path !== undefined || data.paths !== undefined, {
|
|
437
|
-
|
|
426
|
+
error: "Either 'path' or 'paths' must be provided",
|
|
438
427
|
path: ['path'],
|
|
439
428
|
});
|
|
440
429
|
export const CreateDirectoryOutputSchema = z.strictObject({
|
|
@@ -459,15 +448,15 @@ export const EditFileInputSchema = z.strictObject({
|
|
|
459
448
|
.array(z.strictObject({
|
|
460
449
|
oldText: z
|
|
461
450
|
.string()
|
|
462
|
-
.describe('Exact literal string to replace
|
|
451
|
+
.describe('Exact literal string to replace (character-for-character). Include 3–5 lines of context for unique targeting.'),
|
|
463
452
|
newText: z
|
|
464
453
|
.string()
|
|
465
|
-
.describe('Replacement string
|
|
454
|
+
.describe('Replacement string. Preserve surrounding indentation style.'),
|
|
466
455
|
}))
|
|
467
456
|
.min(1, 'Min 1 edit required')
|
|
468
457
|
.describe('List of replacements to apply sequentially. Each edit replaces the first occurrence of oldText.'),
|
|
469
|
-
dryRun: defaultFalseBoolean('Preview edits without writing. Check unmatchedEdits in
|
|
470
|
-
ignoreWhitespace: defaultFalseBoolean('
|
|
458
|
+
dryRun: defaultFalseBoolean('Preview edits without writing. Check `unmatchedEdits` in response.'),
|
|
459
|
+
ignoreWhitespace: defaultFalseBoolean('Treat all whitespace sequences as equivalent when matching oldText.'),
|
|
471
460
|
});
|
|
472
461
|
export const EditFileOutputSchema = z.strictObject({
|
|
473
462
|
ok: z.boolean(),
|
|
@@ -490,7 +479,7 @@ export const MoveFileInputSchema = z
|
|
|
490
479
|
destination: RequiredPathSchema.describe('New path'),
|
|
491
480
|
})
|
|
492
481
|
.refine((data) => (data.source ?? data.sources) !== undefined, {
|
|
493
|
-
|
|
482
|
+
error: "Either 'source' or 'sources' must be provided",
|
|
494
483
|
path: ['source'],
|
|
495
484
|
});
|
|
496
485
|
export const MoveFileOutputSchema = z.strictObject({
|
|
@@ -535,7 +524,6 @@ export const DiffFilesInputSchema = z.strictObject({
|
|
|
535
524
|
original: RequiredPathSchema.describe('Path to original file'),
|
|
536
525
|
modified: RequiredPathSchema.describe('Path to modified file'),
|
|
537
526
|
context: z
|
|
538
|
-
.number()
|
|
539
527
|
.int({ error: 'Must be integer' })
|
|
540
528
|
.min(0, 'Min: 0')
|
|
541
529
|
.max(10000, 'Max: 10,000')
|
|
@@ -564,9 +552,8 @@ export const ApplyPatchInputSchema = z.strictObject({
|
|
|
564
552
|
path: RequiredPathSchema.describe('Path to file to patch'),
|
|
565
553
|
patch: z
|
|
566
554
|
.string()
|
|
567
|
-
.describe('Unified diff
|
|
555
|
+
.describe('Unified diff with @@ hunk headers. Generate with `diff_files`.'),
|
|
568
556
|
fuzzFactor: z
|
|
569
|
-
.number()
|
|
570
557
|
.int({ error: 'Must be integer' })
|
|
571
558
|
.min(0, 'Min: 0')
|
|
572
559
|
.max(20, 'Max: 20')
|
|
@@ -581,7 +568,7 @@ export const ApplyPatchInputSchema = z.strictObject({
|
|
|
581
568
|
.boolean()
|
|
582
569
|
.optional()
|
|
583
570
|
.default(false)
|
|
584
|
-
.describe('Validate
|
|
571
|
+
.describe('Validate patch without writing. Check `applied` before committing.'),
|
|
585
572
|
});
|
|
586
573
|
export const ApplyPatchOutputSchema = z.strictObject({
|
|
587
574
|
ok: z.boolean(),
|
|
@@ -602,18 +589,18 @@ export const SearchAndReplaceInputSchema = z.strictObject({
|
|
|
602
589
|
searchPattern: z
|
|
603
590
|
.string()
|
|
604
591
|
.min(1, 'Search pattern required')
|
|
605
|
-
.describe('Text to search for.
|
|
592
|
+
.describe('Text to search for. Literal by default; RE2 regex when `isRegex=true`.'),
|
|
606
593
|
replacement: z.string().describe('Replacement text'),
|
|
607
|
-
isRegex: defaultFalseBoolean('Treat searchPattern as
|
|
594
|
+
isRegex: defaultFalseBoolean('Treat searchPattern as RE2 regex. Supports capture groups ($1, $2) in replacement.'),
|
|
608
595
|
dryRun: defaultFalseBoolean('Preview matches without writing. Check changedFiles and matches in the response before committing.'),
|
|
609
596
|
includeHidden: z
|
|
610
597
|
.boolean()
|
|
611
598
|
.optional()
|
|
612
|
-
.describe('Include hidden files
|
|
599
|
+
.describe('Include hidden files/directories (starting with .). Default: false.'),
|
|
613
600
|
includeIgnored: z
|
|
614
601
|
.boolean()
|
|
615
602
|
.optional()
|
|
616
|
-
.describe('Include
|
|
603
|
+
.describe('Include .gitignore-ignored files (node_modules, dist). Default: false.'),
|
|
617
604
|
returnDiff: z
|
|
618
605
|
.boolean()
|
|
619
606
|
.optional()
|