@dexto/tools-filesystem 1.5.2 → 1.5.3

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 (37) hide show
  1. package/dist/directory-approval.integration.test.cjs +36 -32
  2. package/dist/directory-approval.integration.test.js +36 -32
  3. package/dist/edit-file-tool.cjs +43 -19
  4. package/dist/edit-file-tool.js +43 -19
  5. package/dist/edit-file-tool.test.cjs +203 -0
  6. package/dist/edit-file-tool.test.d.cts +2 -0
  7. package/dist/edit-file-tool.test.d.ts +2 -0
  8. package/dist/edit-file-tool.test.js +180 -0
  9. package/dist/filesystem-service.cjs +17 -14
  10. package/dist/filesystem-service.d.cts +3 -3
  11. package/dist/filesystem-service.d.ts +3 -3
  12. package/dist/filesystem-service.js +17 -14
  13. package/dist/filesystem-service.test.cjs +233 -0
  14. package/dist/filesystem-service.test.d.cts +2 -0
  15. package/dist/filesystem-service.test.d.ts +2 -0
  16. package/dist/filesystem-service.test.js +210 -0
  17. package/dist/path-validator.cjs +29 -20
  18. package/dist/path-validator.d.cts +9 -2
  19. package/dist/path-validator.d.ts +9 -2
  20. package/dist/path-validator.js +29 -20
  21. package/dist/path-validator.test.cjs +54 -48
  22. package/dist/path-validator.test.js +54 -48
  23. package/dist/read-file-tool.cjs +2 -2
  24. package/dist/read-file-tool.js +2 -2
  25. package/dist/tool-provider.cjs +22 -7
  26. package/dist/tool-provider.d.cts +4 -1
  27. package/dist/tool-provider.d.ts +4 -1
  28. package/dist/tool-provider.js +22 -7
  29. package/dist/types.d.cts +6 -0
  30. package/dist/types.d.ts +6 -0
  31. package/dist/write-file-tool.cjs +41 -7
  32. package/dist/write-file-tool.js +46 -8
  33. package/dist/write-file-tool.test.cjs +217 -0
  34. package/dist/write-file-tool.test.d.cts +2 -0
  35. package/dist/write-file-tool.test.d.ts +2 -0
  36. package/dist/write-file-tool.test.js +194 -0
  37. package/package.json +2 -2
@@ -15,7 +15,7 @@ describe("PathValidator", () => {
15
15
  });
16
16
  describe("validatePath", () => {
17
17
  describe("Empty and Invalid Paths", () => {
18
- it("should reject empty path", () => {
18
+ it("should reject empty path", async () => {
19
19
  const validator = new PathValidator(
20
20
  {
21
21
  allowedPaths: ["/home/user/project"],
@@ -28,11 +28,11 @@ describe("PathValidator", () => {
28
28
  },
29
29
  mockLogger
30
30
  );
31
- const result = validator.validatePath("");
31
+ const result = await validator.validatePath("");
32
32
  expect(result.isValid).toBe(false);
33
33
  expect(result.error).toBe("Path cannot be empty");
34
34
  });
35
- it("should reject whitespace-only path", () => {
35
+ it("should reject whitespace-only path", async () => {
36
36
  const validator = new PathValidator(
37
37
  {
38
38
  allowedPaths: ["/home/user/project"],
@@ -45,13 +45,13 @@ describe("PathValidator", () => {
45
45
  },
46
46
  mockLogger
47
47
  );
48
- const result = validator.validatePath(" ");
48
+ const result = await validator.validatePath(" ");
49
49
  expect(result.isValid).toBe(false);
50
50
  expect(result.error).toBe("Path cannot be empty");
51
51
  });
52
52
  });
53
53
  describe("Allowed Paths", () => {
54
- it("should allow paths within allowed directories", () => {
54
+ it("should allow paths within allowed directories", async () => {
55
55
  const validator = new PathValidator(
56
56
  {
57
57
  allowedPaths: ["/home/user/project"],
@@ -64,11 +64,11 @@ describe("PathValidator", () => {
64
64
  },
65
65
  mockLogger
66
66
  );
67
- const result = validator.validatePath("/home/user/project/src/file.ts");
67
+ const result = await validator.validatePath("/home/user/project/src/file.ts");
68
68
  expect(result.isValid).toBe(true);
69
69
  expect(result.normalizedPath).toBeDefined();
70
70
  });
71
- it("should allow relative paths within working directory", () => {
71
+ it("should allow relative paths within working directory", async () => {
72
72
  const validator = new PathValidator(
73
73
  {
74
74
  allowedPaths: ["/home/user/project"],
@@ -81,10 +81,10 @@ describe("PathValidator", () => {
81
81
  },
82
82
  mockLogger
83
83
  );
84
- const result = validator.validatePath("src/file.ts");
84
+ const result = await validator.validatePath("src/file.ts");
85
85
  expect(result.isValid).toBe(true);
86
86
  });
87
- it("should reject paths outside allowed directories", () => {
87
+ it("should reject paths outside allowed directories", async () => {
88
88
  const validator = new PathValidator(
89
89
  {
90
90
  allowedPaths: ["/home/user/project"],
@@ -97,11 +97,11 @@ describe("PathValidator", () => {
97
97
  },
98
98
  mockLogger
99
99
  );
100
- const result = validator.validatePath("/external/project/file.ts");
100
+ const result = await validator.validatePath("/external/project/file.ts");
101
101
  expect(result.isValid).toBe(false);
102
102
  expect(result.error).toContain("not within allowed paths");
103
103
  });
104
- it("should allow all paths when allowedPaths is empty", () => {
104
+ it("should allow all paths when allowedPaths is empty", async () => {
105
105
  const validator = new PathValidator(
106
106
  {
107
107
  allowedPaths: [],
@@ -114,12 +114,12 @@ describe("PathValidator", () => {
114
114
  },
115
115
  mockLogger
116
116
  );
117
- const result = validator.validatePath("/anywhere/file.ts");
117
+ const result = await validator.validatePath("/anywhere/file.ts");
118
118
  expect(result.isValid).toBe(true);
119
119
  });
120
120
  });
121
121
  describe("Path Traversal Detection", () => {
122
- it("should reject path traversal attempts", () => {
122
+ it("should reject path traversal attempts", async () => {
123
123
  const validator = new PathValidator(
124
124
  {
125
125
  allowedPaths: ["/home/user/project"],
@@ -132,13 +132,15 @@ describe("PathValidator", () => {
132
132
  },
133
133
  mockLogger
134
134
  );
135
- const result = validator.validatePath("/home/user/project/../../../etc/passwd");
135
+ const result = await validator.validatePath(
136
+ "/home/user/project/../../../etc/passwd"
137
+ );
136
138
  expect(result.isValid).toBe(false);
137
139
  expect(result.error).toBe("Path traversal detected");
138
140
  });
139
141
  });
140
142
  describe("Blocked Paths", () => {
141
- it("should reject paths in blocked directories", () => {
143
+ it("should reject paths in blocked directories", async () => {
142
144
  const validator = new PathValidator(
143
145
  {
144
146
  allowedPaths: ["/home/user/project"],
@@ -151,11 +153,11 @@ describe("PathValidator", () => {
151
153
  },
152
154
  mockLogger
153
155
  );
154
- const result = validator.validatePath("/home/user/project/.git/config");
156
+ const result = await validator.validatePath("/home/user/project/.git/config");
155
157
  expect(result.isValid).toBe(false);
156
158
  expect(result.error).toContain("blocked");
157
159
  });
158
- it("should reject paths in node_modules", () => {
160
+ it("should reject paths in node_modules", async () => {
159
161
  const validator = new PathValidator(
160
162
  {
161
163
  allowedPaths: ["/home/user/project"],
@@ -168,7 +170,7 @@ describe("PathValidator", () => {
168
170
  },
169
171
  mockLogger
170
172
  );
171
- const result = validator.validatePath(
173
+ const result = await validator.validatePath(
172
174
  "/home/user/project/node_modules/lodash/index.js"
173
175
  );
174
176
  expect(result.isValid).toBe(false);
@@ -176,7 +178,7 @@ describe("PathValidator", () => {
176
178
  });
177
179
  });
178
180
  describe("Blocked Extensions", () => {
179
- it("should reject files with blocked extensions", () => {
181
+ it("should reject files with blocked extensions", async () => {
180
182
  const validator = new PathValidator(
181
183
  {
182
184
  allowedPaths: ["/home/user/project"],
@@ -189,11 +191,11 @@ describe("PathValidator", () => {
189
191
  },
190
192
  mockLogger
191
193
  );
192
- const result = validator.validatePath("/home/user/project/malware.exe");
194
+ const result = await validator.validatePath("/home/user/project/malware.exe");
193
195
  expect(result.isValid).toBe(false);
194
196
  expect(result.error).toContain(".exe is not allowed");
195
197
  });
196
- it("should handle extensions without leading dot", () => {
198
+ it("should handle extensions without leading dot", async () => {
197
199
  const validator = new PathValidator(
198
200
  {
199
201
  allowedPaths: ["/home/user/project"],
@@ -207,10 +209,10 @@ describe("PathValidator", () => {
207
209
  },
208
210
  mockLogger
209
211
  );
210
- const result = validator.validatePath("/home/user/project/file.exe");
212
+ const result = await validator.validatePath("/home/user/project/file.exe");
211
213
  expect(result.isValid).toBe(false);
212
214
  });
213
- it("should be case-insensitive for extensions", () => {
215
+ it("should be case-insensitive for extensions", async () => {
214
216
  const validator = new PathValidator(
215
217
  {
216
218
  allowedPaths: ["/home/user/project"],
@@ -223,12 +225,12 @@ describe("PathValidator", () => {
223
225
  },
224
226
  mockLogger
225
227
  );
226
- const result = validator.validatePath("/home/user/project/file.EXE");
228
+ const result = await validator.validatePath("/home/user/project/file.EXE");
227
229
  expect(result.isValid).toBe(false);
228
230
  });
229
231
  });
230
232
  describe("Directory Approval Checker Integration", () => {
231
- it("should consult approval checker for external paths", () => {
233
+ it("should consult approval checker for external paths", async () => {
232
234
  const validator = new PathValidator(
233
235
  {
234
236
  allowedPaths: ["/home/user/project"],
@@ -241,16 +243,16 @@ describe("PathValidator", () => {
241
243
  },
242
244
  mockLogger
243
245
  );
244
- let result = validator.validatePath("/external/project/file.ts");
246
+ let result = await validator.validatePath("/external/project/file.ts");
245
247
  expect(result.isValid).toBe(false);
246
248
  const approvalChecker = (filePath) => {
247
249
  return filePath.startsWith("/external/project");
248
250
  };
249
251
  validator.setDirectoryApprovalChecker(approvalChecker);
250
- result = validator.validatePath("/external/project/file.ts");
252
+ result = await validator.validatePath("/external/project/file.ts");
251
253
  expect(result.isValid).toBe(true);
252
254
  });
253
- it("should not use approval checker for config-allowed paths", () => {
255
+ it("should not use approval checker for config-allowed paths", async () => {
254
256
  const approvalChecker = vi.fn().mockReturnValue(false);
255
257
  const validator = new PathValidator(
256
258
  {
@@ -265,14 +267,14 @@ describe("PathValidator", () => {
265
267
  mockLogger
266
268
  );
267
269
  validator.setDirectoryApprovalChecker(approvalChecker);
268
- const result = validator.validatePath("/home/user/project/src/file.ts");
270
+ const result = await validator.validatePath("/home/user/project/src/file.ts");
269
271
  expect(result.isValid).toBe(true);
270
272
  expect(approvalChecker).not.toHaveBeenCalled();
271
273
  });
272
274
  });
273
275
  });
274
276
  describe("isPathWithinAllowed", () => {
275
- it("should return true for paths within config-allowed directories", () => {
277
+ it("should return true for paths within config-allowed directories", async () => {
276
278
  const validator = new PathValidator(
277
279
  {
278
280
  allowedPaths: ["/home/user/project"],
@@ -285,12 +287,14 @@ describe("PathValidator", () => {
285
287
  },
286
288
  mockLogger
287
289
  );
288
- expect(validator.isPathWithinAllowed("/home/user/project/src/file.ts")).toBe(true);
289
- expect(validator.isPathWithinAllowed("/home/user/project/deep/nested/file.ts")).toBe(
290
+ expect(await validator.isPathWithinAllowed("/home/user/project/src/file.ts")).toBe(
290
291
  true
291
292
  );
293
+ expect(
294
+ await validator.isPathWithinAllowed("/home/user/project/deep/nested/file.ts")
295
+ ).toBe(true);
292
296
  });
293
- it("should return false for paths outside config-allowed directories", () => {
297
+ it("should return false for paths outside config-allowed directories", async () => {
294
298
  const validator = new PathValidator(
295
299
  {
296
300
  allowedPaths: ["/home/user/project"],
@@ -303,10 +307,10 @@ describe("PathValidator", () => {
303
307
  },
304
308
  mockLogger
305
309
  );
306
- expect(validator.isPathWithinAllowed("/external/project/file.ts")).toBe(false);
307
- expect(validator.isPathWithinAllowed("/home/user/other/file.ts")).toBe(false);
310
+ expect(await validator.isPathWithinAllowed("/external/project/file.ts")).toBe(false);
311
+ expect(await validator.isPathWithinAllowed("/home/user/other/file.ts")).toBe(false);
308
312
  });
309
- it("should NOT consult approval checker (used for prompting decisions)", () => {
313
+ it("should NOT consult approval checker (used for prompting decisions)", async () => {
310
314
  const approvalChecker = vi.fn().mockReturnValue(true);
311
315
  const validator = new PathValidator(
312
316
  {
@@ -321,10 +325,10 @@ describe("PathValidator", () => {
321
325
  mockLogger
322
326
  );
323
327
  validator.setDirectoryApprovalChecker(approvalChecker);
324
- expect(validator.isPathWithinAllowed("/external/project/file.ts")).toBe(false);
328
+ expect(await validator.isPathWithinAllowed("/external/project/file.ts")).toBe(false);
325
329
  expect(approvalChecker).not.toHaveBeenCalled();
326
330
  });
327
- it("should return false for empty path", () => {
331
+ it("should return false for empty path", async () => {
328
332
  const validator = new PathValidator(
329
333
  {
330
334
  allowedPaths: ["/home/user/project"],
@@ -337,10 +341,10 @@ describe("PathValidator", () => {
337
341
  },
338
342
  mockLogger
339
343
  );
340
- expect(validator.isPathWithinAllowed("")).toBe(false);
341
- expect(validator.isPathWithinAllowed(" ")).toBe(false);
344
+ expect(await validator.isPathWithinAllowed("")).toBe(false);
345
+ expect(await validator.isPathWithinAllowed(" ")).toBe(false);
342
346
  });
343
- it("should return true when allowedPaths is empty (all paths allowed)", () => {
347
+ it("should return true when allowedPaths is empty (all paths allowed)", async () => {
344
348
  const validator = new PathValidator(
345
349
  {
346
350
  allowedPaths: [],
@@ -353,11 +357,11 @@ describe("PathValidator", () => {
353
357
  },
354
358
  mockLogger
355
359
  );
356
- expect(validator.isPathWithinAllowed("/anywhere/file.ts")).toBe(true);
360
+ expect(await validator.isPathWithinAllowed("/anywhere/file.ts")).toBe(true);
357
361
  });
358
362
  });
359
363
  describe("Path Containment (Parent Directory Coverage)", () => {
360
- it("should recognize that approving parent covers child paths", () => {
364
+ it("should recognize that approving parent covers child paths", async () => {
361
365
  const validator = new PathValidator(
362
366
  {
363
367
  allowedPaths: ["/external/sub"],
@@ -370,9 +374,11 @@ describe("PathValidator", () => {
370
374
  },
371
375
  mockLogger
372
376
  );
373
- expect(validator.isPathWithinAllowed("/external/sub/deep/nested/file.ts")).toBe(true);
377
+ expect(await validator.isPathWithinAllowed("/external/sub/deep/nested/file.ts")).toBe(
378
+ true
379
+ );
374
380
  });
375
- it("should not allow sibling directories", () => {
381
+ it("should not allow sibling directories", async () => {
376
382
  const validator = new PathValidator(
377
383
  {
378
384
  allowedPaths: ["/external/sub"],
@@ -385,9 +391,9 @@ describe("PathValidator", () => {
385
391
  },
386
392
  mockLogger
387
393
  );
388
- expect(validator.isPathWithinAllowed("/external/other/file.ts")).toBe(false);
394
+ expect(await validator.isPathWithinAllowed("/external/other/file.ts")).toBe(false);
389
395
  });
390
- it("should not allow parent directories when child is approved", () => {
396
+ it("should not allow parent directories when child is approved", async () => {
391
397
  const validator = new PathValidator(
392
398
  {
393
399
  allowedPaths: ["/external/sub/deep"],
@@ -400,7 +406,7 @@ describe("PathValidator", () => {
400
406
  },
401
407
  mockLogger
402
408
  );
403
- expect(validator.isPathWithinAllowed("/external/sub/file.ts")).toBe(false);
409
+ expect(await validator.isPathWithinAllowed("/external/sub/file.ts")).toBe(false);
404
410
  });
405
411
  });
406
412
  describe("getAllowedPaths and getBlockedPaths", () => {
@@ -50,10 +50,10 @@ function createReadFileTool(options) {
50
50
  * Check if this read operation needs directory access approval.
51
51
  * Returns custom approval request if the file is outside allowed paths.
52
52
  */
53
- getApprovalOverride: (args) => {
53
+ getApprovalOverride: async (args) => {
54
54
  const { file_path } = args;
55
55
  if (!file_path) return null;
56
- const isAllowed = fileSystemService.isPathWithinConfigAllowed(file_path);
56
+ const isAllowed = await fileSystemService.isPathWithinConfigAllowed(file_path);
57
57
  if (isAllowed) {
58
58
  return null;
59
59
  }
@@ -17,10 +17,10 @@ function createReadFileTool(options) {
17
17
  * Check if this read operation needs directory access approval.
18
18
  * Returns custom approval request if the file is outside allowed paths.
19
19
  */
20
- getApprovalOverride: (args) => {
20
+ getApprovalOverride: async (args) => {
21
21
  const { file_path } = args;
22
22
  if (!file_path) return null;
23
- const isAllowed = fileSystemService.isPathWithinConfigAllowed(file_path);
23
+ const isAllowed = await fileSystemService.isPathWithinConfigAllowed(file_path);
24
24
  if (isAllowed) {
25
25
  return null;
26
26
  }
@@ -34,6 +34,13 @@ const DEFAULT_BLOCKED_EXTENSIONS = [".exe", ".dll", ".so"];
34
34
  const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
35
35
  const DEFAULT_ENABLE_BACKUPS = false;
36
36
  const DEFAULT_BACKUP_RETENTION_DAYS = 7;
37
+ const FILESYSTEM_TOOL_NAMES = [
38
+ "read_file",
39
+ "write_file",
40
+ "edit_file",
41
+ "glob_files",
42
+ "grep_content"
43
+ ];
37
44
  const FileSystemToolsConfigSchema = import_zod.z.object({
38
45
  type: import_zod.z.literal("filesystem-tools"),
39
46
  allowedPaths: import_zod.z.array(import_zod.z.string()).default(DEFAULT_ALLOWED_PATHS).describe("List of allowed base paths for file operations"),
@@ -47,6 +54,9 @@ const FileSystemToolsConfigSchema = import_zod.z.object({
47
54
  backupPath: import_zod.z.string().optional().describe("Absolute path for storing file backups (if enableBackups is true)"),
48
55
  backupRetentionDays: import_zod.z.number().int().positive().default(DEFAULT_BACKUP_RETENTION_DAYS).describe(
49
56
  `Number of days to retain backup files (default: ${DEFAULT_BACKUP_RETENTION_DAYS})`
57
+ ),
58
+ enabledTools: import_zod.z.array(import_zod.z.enum(FILESYSTEM_TOOL_NAMES)).optional().describe(
59
+ `Subset of tools to enable. If not specified, all tools are enabled. Available: ${FILESYSTEM_TOOL_NAMES.join(", ")}`
50
60
  )
51
61
  }).strict();
52
62
  const fileSystemToolsProvider = {
@@ -88,13 +98,18 @@ const fileSystemToolsProvider = {
88
98
  fileSystemService,
89
99
  directoryApproval
90
100
  };
91
- return [
92
- (0, import_read_file_tool.createReadFileTool)(fileToolOptions),
93
- (0, import_write_file_tool.createWriteFileTool)(fileToolOptions),
94
- (0, import_edit_file_tool.createEditFileTool)(fileToolOptions),
95
- (0, import_glob_files_tool.createGlobFilesTool)(fileSystemService),
96
- (0, import_grep_content_tool.createGrepContentTool)(fileSystemService)
97
- ];
101
+ const toolCreators = {
102
+ read_file: () => (0, import_read_file_tool.createReadFileTool)(fileToolOptions),
103
+ write_file: () => (0, import_write_file_tool.createWriteFileTool)(fileToolOptions),
104
+ edit_file: () => (0, import_edit_file_tool.createEditFileTool)(fileToolOptions),
105
+ glob_files: () => (0, import_glob_files_tool.createGlobFilesTool)(fileSystemService),
106
+ grep_content: () => (0, import_grep_content_tool.createGrepContentTool)(fileSystemService)
107
+ };
108
+ const toolsToCreate = config.enabledTools ?? FILESYSTEM_TOOL_NAMES;
109
+ if (config.enabledTools) {
110
+ logger.debug(`Creating subset of filesystem tools: ${toolsToCreate.join(", ")}`);
111
+ }
112
+ return toolsToCreate.map((toolName) => toolCreators[toolName]());
98
113
  },
99
114
  metadata: {
100
115
  displayName: "FileSystem Tools",
@@ -34,16 +34,18 @@ declare const FileSystemToolsConfigSchema: z.ZodObject<{
34
34
  enableBackups: z.ZodDefault<z.ZodBoolean>;
35
35
  backupPath: z.ZodOptional<z.ZodString>;
36
36
  backupRetentionDays: z.ZodDefault<z.ZodNumber>;
37
+ enabledTools: z.ZodOptional<z.ZodArray<z.ZodEnum<["read_file", "write_file", "edit_file", "glob_files", "grep_content"]>, "many">>;
37
38
  }, "strict", z.ZodTypeAny, {
38
- type: "filesystem-tools";
39
39
  allowedPaths: string[];
40
40
  blockedExtensions: string[];
41
41
  blockedPaths: string[];
42
42
  maxFileSize: number;
43
43
  enableBackups: boolean;
44
44
  backupRetentionDays: number;
45
+ type: "filesystem-tools";
45
46
  backupPath?: string | undefined;
46
47
  workingDirectory?: string | undefined;
48
+ enabledTools?: ("edit_file" | "glob_files" | "grep_content" | "read_file" | "write_file")[] | undefined;
47
49
  }, {
48
50
  type: "filesystem-tools";
49
51
  allowedPaths?: string[] | undefined;
@@ -54,6 +56,7 @@ declare const FileSystemToolsConfigSchema: z.ZodObject<{
54
56
  enableBackups?: boolean | undefined;
55
57
  backupRetentionDays?: number | undefined;
56
58
  workingDirectory?: string | undefined;
59
+ enabledTools?: ("edit_file" | "glob_files" | "grep_content" | "read_file" | "write_file")[] | undefined;
57
60
  }>;
58
61
  type FileSystemToolsConfig = z.output<typeof FileSystemToolsConfigSchema>;
59
62
  /**
@@ -34,16 +34,18 @@ declare const FileSystemToolsConfigSchema: z.ZodObject<{
34
34
  enableBackups: z.ZodDefault<z.ZodBoolean>;
35
35
  backupPath: z.ZodOptional<z.ZodString>;
36
36
  backupRetentionDays: z.ZodDefault<z.ZodNumber>;
37
+ enabledTools: z.ZodOptional<z.ZodArray<z.ZodEnum<["read_file", "write_file", "edit_file", "glob_files", "grep_content"]>, "many">>;
37
38
  }, "strict", z.ZodTypeAny, {
38
- type: "filesystem-tools";
39
39
  allowedPaths: string[];
40
40
  blockedExtensions: string[];
41
41
  blockedPaths: string[];
42
42
  maxFileSize: number;
43
43
  enableBackups: boolean;
44
44
  backupRetentionDays: number;
45
+ type: "filesystem-tools";
45
46
  backupPath?: string | undefined;
46
47
  workingDirectory?: string | undefined;
48
+ enabledTools?: ("edit_file" | "glob_files" | "grep_content" | "read_file" | "write_file")[] | undefined;
47
49
  }, {
48
50
  type: "filesystem-tools";
49
51
  allowedPaths?: string[] | undefined;
@@ -54,6 +56,7 @@ declare const FileSystemToolsConfigSchema: z.ZodObject<{
54
56
  enableBackups?: boolean | undefined;
55
57
  backupRetentionDays?: number | undefined;
56
58
  workingDirectory?: string | undefined;
59
+ enabledTools?: ("edit_file" | "glob_files" | "grep_content" | "read_file" | "write_file")[] | undefined;
57
60
  }>;
58
61
  type FileSystemToolsConfig = z.output<typeof FileSystemToolsConfigSchema>;
59
62
  /**
@@ -11,6 +11,13 @@ const DEFAULT_BLOCKED_EXTENSIONS = [".exe", ".dll", ".so"];
11
11
  const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
12
12
  const DEFAULT_ENABLE_BACKUPS = false;
13
13
  const DEFAULT_BACKUP_RETENTION_DAYS = 7;
14
+ const FILESYSTEM_TOOL_NAMES = [
15
+ "read_file",
16
+ "write_file",
17
+ "edit_file",
18
+ "glob_files",
19
+ "grep_content"
20
+ ];
14
21
  const FileSystemToolsConfigSchema = z.object({
15
22
  type: z.literal("filesystem-tools"),
16
23
  allowedPaths: z.array(z.string()).default(DEFAULT_ALLOWED_PATHS).describe("List of allowed base paths for file operations"),
@@ -24,6 +31,9 @@ const FileSystemToolsConfigSchema = z.object({
24
31
  backupPath: z.string().optional().describe("Absolute path for storing file backups (if enableBackups is true)"),
25
32
  backupRetentionDays: z.number().int().positive().default(DEFAULT_BACKUP_RETENTION_DAYS).describe(
26
33
  `Number of days to retain backup files (default: ${DEFAULT_BACKUP_RETENTION_DAYS})`
34
+ ),
35
+ enabledTools: z.array(z.enum(FILESYSTEM_TOOL_NAMES)).optional().describe(
36
+ `Subset of tools to enable. If not specified, all tools are enabled. Available: ${FILESYSTEM_TOOL_NAMES.join(", ")}`
27
37
  )
28
38
  }).strict();
29
39
  const fileSystemToolsProvider = {
@@ -65,13 +75,18 @@ const fileSystemToolsProvider = {
65
75
  fileSystemService,
66
76
  directoryApproval
67
77
  };
68
- return [
69
- createReadFileTool(fileToolOptions),
70
- createWriteFileTool(fileToolOptions),
71
- createEditFileTool(fileToolOptions),
72
- createGlobFilesTool(fileSystemService),
73
- createGrepContentTool(fileSystemService)
74
- ];
78
+ const toolCreators = {
79
+ read_file: () => createReadFileTool(fileToolOptions),
80
+ write_file: () => createWriteFileTool(fileToolOptions),
81
+ edit_file: () => createEditFileTool(fileToolOptions),
82
+ glob_files: () => createGlobFilesTool(fileSystemService),
83
+ grep_content: () => createGrepContentTool(fileSystemService)
84
+ };
85
+ const toolsToCreate = config.enabledTools ?? FILESYSTEM_TOOL_NAMES;
86
+ if (config.enabledTools) {
87
+ logger.debug(`Creating subset of filesystem tools: ${toolsToCreate.join(", ")}`);
88
+ }
89
+ return toolsToCreate.map((toolName) => toolCreators[toolName]());
75
90
  },
76
91
  metadata: {
77
92
  displayName: "FileSystem Tools",
package/dist/types.d.cts CHANGED
@@ -112,6 +112,8 @@ interface WriteResult {
112
112
  path: string;
113
113
  bytesWritten: number;
114
114
  backupPath?: string | undefined;
115
+ /** Original content if file was overwritten (undefined for new files) */
116
+ originalContent?: string | undefined;
115
117
  }
116
118
  /**
117
119
  * Edit operation
@@ -138,6 +140,10 @@ interface EditResult {
138
140
  path: string;
139
141
  changesCount: number;
140
142
  backupPath?: string | undefined;
143
+ /** Original content before edit (for diff generation) */
144
+ originalContent: string;
145
+ /** New content after edit (for diff generation) */
146
+ newContent: string;
141
147
  }
142
148
  /**
143
149
  * Path validation result
package/dist/types.d.ts CHANGED
@@ -112,6 +112,8 @@ interface WriteResult {
112
112
  path: string;
113
113
  bytesWritten: number;
114
114
  backupPath?: string | undefined;
115
+ /** Original content if file was overwritten (undefined for new files) */
116
+ originalContent?: string | undefined;
115
117
  }
116
118
  /**
117
119
  * Edit operation
@@ -138,6 +140,10 @@ interface EditResult {
138
140
  path: string;
139
141
  changesCount: number;
140
142
  backupPath?: string | undefined;
143
+ /** Original content before edit (for diff generation) */
144
+ originalContent: string;
145
+ /** New content after edit (for diff generation) */
146
+ newContent: string;
141
147
  }
142
148
  /**
143
149
  * Path validation result
@@ -32,10 +32,16 @@ __export(write_file_tool_exports, {
32
32
  });
33
33
  module.exports = __toCommonJS(write_file_tool_exports);
34
34
  var path = __toESM(require("node:path"), 1);
35
+ var import_node_crypto = require("node:crypto");
35
36
  var import_zod = require("zod");
36
37
  var import_diff = require("diff");
37
38
  var import_core = require("@dexto/core");
38
39
  var import_error_codes = require("./error-codes.js");
40
+ const previewContentHashCache = /* @__PURE__ */ new Map();
41
+ const FILE_NOT_EXISTS_MARKER = null;
42
+ function computeContentHash(content) {
43
+ return (0, import_node_crypto.createHash)("sha256").update(content, "utf8").digest("hex");
44
+ }
39
45
  const WriteFileInputSchema = import_zod.z.object({
40
46
  file_path: import_zod.z.string().describe("Absolute path where the file should be written"),
41
47
  content: import_zod.z.string().describe("Content to write to the file"),
@@ -67,10 +73,10 @@ function createWriteFileTool(options) {
67
73
  * Check if this write operation needs directory access approval.
68
74
  * Returns custom approval request if the file is outside allowed paths.
69
75
  */
70
- getApprovalOverride: (args) => {
76
+ getApprovalOverride: async (args) => {
71
77
  const { file_path } = args;
72
78
  if (!file_path) return null;
73
- const isAllowed = fileSystemService.isPathWithinConfigAllowed(file_path);
79
+ const isAllowed = await fileSystemService.isPathWithinConfigAllowed(file_path);
74
80
  if (isAllowed) {
75
81
  return null;
76
82
  }
@@ -105,15 +111,25 @@ function createWriteFileTool(options) {
105
111
  },
106
112
  /**
107
113
  * Generate preview for approval UI - shows diff or file creation info
114
+ * Stores content hash for change detection in execute phase.
108
115
  */
109
- generatePreview: async (input, _context) => {
116
+ generatePreview: async (input, context) => {
110
117
  const { file_path, content } = input;
111
118
  try {
112
119
  const originalFile = await fileSystemService.readFile(file_path);
113
120
  const originalContent = originalFile.content;
121
+ if (context?.toolCallId) {
122
+ previewContentHashCache.set(
123
+ context.toolCallId,
124
+ computeContentHash(originalContent)
125
+ );
126
+ }
114
127
  return generateDiffPreview(file_path, originalContent, content);
115
128
  } catch (error) {
116
129
  if (error instanceof import_core.DextoRuntimeError && error.code === import_error_codes.FileSystemErrorCode.FILE_NOT_FOUND) {
130
+ if (context?.toolCallId) {
131
+ previewContentHashCache.set(context.toolCallId, FILE_NOT_EXISTS_MARKER);
132
+ }
117
133
  const lineCount = content.split("\n").length;
118
134
  const preview = {
119
135
  type: "file",
@@ -129,24 +145,42 @@ function createWriteFileTool(options) {
129
145
  throw error;
130
146
  }
131
147
  },
132
- execute: async (input, _context) => {
148
+ execute: async (input, context) => {
133
149
  const { file_path, content, create_dirs, encoding } = input;
134
150
  let originalContent = null;
151
+ let fileExistsNow = false;
135
152
  try {
136
153
  const originalFile = await fileSystemService.readFile(file_path);
137
154
  originalContent = originalFile.content;
155
+ fileExistsNow = true;
138
156
  } catch (error) {
139
157
  if (error instanceof import_core.DextoRuntimeError && error.code === import_error_codes.FileSystemErrorCode.FILE_NOT_FOUND) {
140
158
  originalContent = null;
159
+ fileExistsNow = false;
141
160
  } else {
142
161
  throw error;
143
162
  }
144
163
  }
164
+ if (context?.toolCallId && previewContentHashCache.has(context.toolCallId)) {
165
+ const expectedHash = previewContentHashCache.get(context.toolCallId);
166
+ previewContentHashCache.delete(context.toolCallId);
167
+ if (expectedHash === FILE_NOT_EXISTS_MARKER) {
168
+ if (fileExistsNow) {
169
+ throw import_core.ToolError.fileModifiedSincePreview("write_file", file_path);
170
+ }
171
+ } else if (expectedHash !== null) {
172
+ if (!fileExistsNow) {
173
+ throw import_core.ToolError.fileModifiedSincePreview("write_file", file_path);
174
+ }
175
+ const currentHash = computeContentHash(originalContent);
176
+ if (expectedHash !== currentHash) {
177
+ throw import_core.ToolError.fileModifiedSincePreview("write_file", file_path);
178
+ }
179
+ }
180
+ }
145
181
  const result = await fileSystemService.writeFile(file_path, content, {
146
182
  createDirs: create_dirs,
147
- encoding,
148
- backup: true
149
- // Always create backup for internal tools
183
+ encoding
150
184
  });
151
185
  let _display;
152
186
  if (originalContent === null) {