@dexto/tools-filesystem 1.5.2 → 1.5.4

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 (45) 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 +24 -14
  10. package/dist/filesystem-service.d.cts +8 -3
  11. package/dist/filesystem-service.d.ts +8 -3
  12. package/dist/filesystem-service.js +24 -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/glob-files-tool.cjs +56 -3
  18. package/dist/glob-files-tool.d.cts +4 -3
  19. package/dist/glob-files-tool.d.ts +4 -3
  20. package/dist/glob-files-tool.js +46 -3
  21. package/dist/grep-content-tool.cjs +55 -3
  22. package/dist/grep-content-tool.d.cts +4 -3
  23. package/dist/grep-content-tool.d.ts +4 -3
  24. package/dist/grep-content-tool.js +45 -3
  25. package/dist/path-validator.cjs +29 -20
  26. package/dist/path-validator.d.cts +9 -2
  27. package/dist/path-validator.d.ts +9 -2
  28. package/dist/path-validator.js +29 -20
  29. package/dist/path-validator.test.cjs +54 -48
  30. package/dist/path-validator.test.js +54 -48
  31. package/dist/read-file-tool.cjs +2 -2
  32. package/dist/read-file-tool.js +2 -2
  33. package/dist/tool-provider.cjs +22 -7
  34. package/dist/tool-provider.d.cts +4 -1
  35. package/dist/tool-provider.d.ts +4 -1
  36. package/dist/tool-provider.js +22 -7
  37. package/dist/types.d.cts +6 -0
  38. package/dist/types.d.ts +6 -0
  39. package/dist/write-file-tool.cjs +41 -7
  40. package/dist/write-file-tool.js +46 -8
  41. package/dist/write-file-tool.test.cjs +217 -0
  42. package/dist/write-file-tool.test.d.cts +2 -0
  43. package/dist/write-file-tool.test.d.ts +2 -0
  44. package/dist/write-file-tool.test.js +194 -0
  45. package/package.json +2 -2
@@ -33,6 +33,13 @@ class FileSystemService {
33
33
  getBackupDir() {
34
34
  return this.config.backupPath || getDextoPath("backups");
35
35
  }
36
+ /**
37
+ * Get the effective working directory for file operations.
38
+ * Falls back to process.cwd() if not configured.
39
+ */
40
+ getWorkingDirectory() {
41
+ return this.config.workingDirectory || process.cwd();
42
+ }
36
43
  /**
37
44
  * Initialize the service.
38
45
  * Safe to call multiple times - subsequent calls return the same promise.
@@ -93,7 +100,7 @@ class FileSystemService {
93
100
  * @param filePath The file path to check (can be relative or absolute)
94
101
  * @returns true if the path is within config-allowed paths, false otherwise
95
102
  */
96
- isPathWithinConfigAllowed(filePath) {
103
+ async isPathWithinConfigAllowed(filePath) {
97
104
  return this.pathValidator.isPathWithinAllowed(filePath);
98
105
  }
99
106
  /**
@@ -101,7 +108,7 @@ class FileSystemService {
101
108
  */
102
109
  async readFile(filePath, options = {}) {
103
110
  await this.ensureInitialized();
104
- const validation = this.pathValidator.validatePath(filePath);
111
+ const validation = await this.pathValidator.validatePath(filePath);
105
112
  if (!validation.isValid || !validation.normalizedPath) {
106
113
  throw FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
107
114
  }
@@ -178,7 +185,7 @@ class FileSystemService {
178
185
  });
179
186
  const validFiles = [];
180
187
  for (const file of files) {
181
- const validation = this.pathValidator.validatePath(file);
188
+ const validation = await this.pathValidator.validatePath(file);
182
189
  if (!validation.isValid || !validation.normalizedPath) {
183
190
  this.logger.debug(`Skipping invalid path: ${file}`);
184
191
  continue;
@@ -311,7 +318,7 @@ class FileSystemService {
311
318
  */
312
319
  async writeFile(filePath, content, options = {}) {
313
320
  await this.ensureInitialized();
314
- const validation = this.pathValidator.validatePath(filePath);
321
+ const validation = await this.pathValidator.validatePath(filePath);
315
322
  if (!validation.isValid || !validation.normalizedPath) {
316
323
  throw FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
317
324
  }
@@ -353,14 +360,14 @@ class FileSystemService {
353
360
  */
354
361
  async editFile(filePath, operation, options = {}) {
355
362
  await this.ensureInitialized();
356
- const validation = this.pathValidator.validatePath(filePath);
363
+ const validation = await this.pathValidator.validatePath(filePath);
357
364
  if (!validation.isValid || !validation.normalizedPath) {
358
365
  throw FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
359
366
  }
360
367
  const normalizedPath = validation.normalizedPath;
361
368
  const fileContent = await this.readFile(normalizedPath);
362
- let content = fileContent.content;
363
- const occurrences = (content.match(
369
+ const originalContent = fileContent.content;
370
+ const occurrences = (originalContent.match(
364
371
  new RegExp(operation.oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g")
365
372
  ) || []).length;
366
373
  if (occurrences === 0) {
@@ -374,21 +381,24 @@ class FileSystemService {
374
381
  backupPath = await this.createBackup(normalizedPath);
375
382
  }
376
383
  try {
384
+ let newContent;
377
385
  if (operation.replaceAll) {
378
- content = content.replace(
386
+ newContent = originalContent.replace(
379
387
  new RegExp(operation.oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
380
388
  operation.newString
381
389
  );
382
390
  } else {
383
- content = content.replace(operation.oldString, operation.newString);
391
+ newContent = originalContent.replace(operation.oldString, operation.newString);
384
392
  }
385
- await fs.writeFile(normalizedPath, content, options.encoding || DEFAULT_ENCODING);
393
+ await fs.writeFile(normalizedPath, newContent, options.encoding || DEFAULT_ENCODING);
386
394
  this.logger.debug(`File edited: ${normalizedPath} (${occurrences} replacements)`);
387
395
  return {
388
396
  success: true,
389
397
  path: normalizedPath,
390
398
  changesCount: occurrences,
391
- backupPath
399
+ backupPath,
400
+ originalContent,
401
+ newContent
392
402
  };
393
403
  } catch (error) {
394
404
  throw FileSystemError.editFailed(
@@ -480,10 +490,10 @@ class FileSystemService {
480
490
  return { ...this.config };
481
491
  }
482
492
  /**
483
- * Check if a path is allowed
493
+ * Check if a path is allowed (async for non-blocking symlink resolution)
484
494
  */
485
- isPathAllowed(filePath) {
486
- const validation = this.pathValidator.validatePath(filePath);
495
+ async isPathAllowed(filePath) {
496
+ const validation = await this.pathValidator.validatePath(filePath);
487
497
  return validation.isValid;
488
498
  }
489
499
  }
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+ var import_vitest = require("vitest");
25
+ var path = __toESM(require("node:path"), 1);
26
+ var fs = __toESM(require("node:fs/promises"), 1);
27
+ var os = __toESM(require("node:os"), 1);
28
+ var import_filesystem_service = require("./filesystem-service.js");
29
+ const createMockLogger = () => ({
30
+ debug: import_vitest.vi.fn(),
31
+ info: import_vitest.vi.fn(),
32
+ warn: import_vitest.vi.fn(),
33
+ error: import_vitest.vi.fn(),
34
+ createChild: import_vitest.vi.fn().mockReturnThis()
35
+ });
36
+ (0, import_vitest.describe)("FileSystemService", () => {
37
+ let mockLogger;
38
+ let tempDir;
39
+ (0, import_vitest.beforeEach)(async () => {
40
+ mockLogger = createMockLogger();
41
+ const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-fs-test-"));
42
+ tempDir = await fs.realpath(rawTempDir);
43
+ import_vitest.vi.clearAllMocks();
44
+ });
45
+ (0, import_vitest.afterEach)(async () => {
46
+ try {
47
+ await fs.rm(tempDir, { recursive: true, force: true });
48
+ } catch {
49
+ }
50
+ });
51
+ (0, import_vitest.describe)("Backup Behavior", () => {
52
+ (0, import_vitest.describe)("writeFile", () => {
53
+ (0, import_vitest.it)("should NOT create backup when enableBackups is false (default)", async () => {
54
+ const fileSystemService = new import_filesystem_service.FileSystemService(
55
+ {
56
+ allowedPaths: [tempDir],
57
+ blockedPaths: [],
58
+ blockedExtensions: [],
59
+ maxFileSize: 10 * 1024 * 1024,
60
+ workingDirectory: tempDir,
61
+ enableBackups: false,
62
+ backupRetentionDays: 7
63
+ },
64
+ mockLogger
65
+ );
66
+ await fileSystemService.initialize();
67
+ const testFile = path.join(tempDir, "test.txt");
68
+ await fs.writeFile(testFile, "original content");
69
+ const result = await fileSystemService.writeFile(testFile, "new content");
70
+ (0, import_vitest.expect)(result.success).toBe(true);
71
+ (0, import_vitest.expect)(result.backupPath).toBeUndefined();
72
+ const backupDir = path.join(tempDir, ".dexto-backups");
73
+ try {
74
+ const files = await fs.readdir(backupDir);
75
+ (0, import_vitest.expect)(files.length).toBe(0);
76
+ } catch {
77
+ }
78
+ });
79
+ (0, import_vitest.it)("should create backup when enableBackups is true", async () => {
80
+ const fileSystemService = new import_filesystem_service.FileSystemService(
81
+ {
82
+ allowedPaths: [tempDir],
83
+ blockedPaths: [],
84
+ blockedExtensions: [],
85
+ maxFileSize: 10 * 1024 * 1024,
86
+ workingDirectory: tempDir,
87
+ enableBackups: true,
88
+ backupRetentionDays: 7
89
+ },
90
+ mockLogger
91
+ );
92
+ await fileSystemService.initialize();
93
+ const testFile = path.join(tempDir, "test.txt");
94
+ await fs.writeFile(testFile, "original content");
95
+ const result = await fileSystemService.writeFile(testFile, "new content");
96
+ (0, import_vitest.expect)(result.success).toBe(true);
97
+ (0, import_vitest.expect)(result.backupPath).toBeDefined();
98
+ (0, import_vitest.expect)(result.backupPath).toContain(".dexto");
99
+ (0, import_vitest.expect)(result.backupPath).toContain("backup");
100
+ const backupContent = await fs.readFile(result.backupPath, "utf-8");
101
+ (0, import_vitest.expect)(backupContent).toBe("original content");
102
+ const newContent = await fs.readFile(testFile, "utf-8");
103
+ (0, import_vitest.expect)(newContent).toBe("new content");
104
+ });
105
+ (0, import_vitest.it)("should NOT create backup for new files even when enableBackups is true", async () => {
106
+ const fileSystemService = new import_filesystem_service.FileSystemService(
107
+ {
108
+ allowedPaths: [tempDir],
109
+ blockedPaths: [],
110
+ blockedExtensions: [],
111
+ maxFileSize: 10 * 1024 * 1024,
112
+ workingDirectory: tempDir,
113
+ enableBackups: true,
114
+ backupRetentionDays: 7
115
+ },
116
+ mockLogger
117
+ );
118
+ await fileSystemService.initialize();
119
+ const testFile = path.join(tempDir, "new-file.txt");
120
+ const result = await fileSystemService.writeFile(testFile, "content", {
121
+ createDirs: true
122
+ });
123
+ (0, import_vitest.expect)(result.success).toBe(true);
124
+ (0, import_vitest.expect)(result.backupPath).toBeUndefined();
125
+ });
126
+ (0, import_vitest.it)("should respect per-call backup option over config", async () => {
127
+ const fileSystemService = new import_filesystem_service.FileSystemService(
128
+ {
129
+ allowedPaths: [tempDir],
130
+ blockedPaths: [],
131
+ blockedExtensions: [],
132
+ maxFileSize: 10 * 1024 * 1024,
133
+ workingDirectory: tempDir,
134
+ enableBackups: false,
135
+ // Config says no backups
136
+ backupRetentionDays: 7
137
+ },
138
+ mockLogger
139
+ );
140
+ await fileSystemService.initialize();
141
+ const testFile = path.join(tempDir, "test.txt");
142
+ await fs.writeFile(testFile, "original content");
143
+ const result = await fileSystemService.writeFile(testFile, "new content", {
144
+ backup: true
145
+ });
146
+ (0, import_vitest.expect)(result.success).toBe(true);
147
+ (0, import_vitest.expect)(result.backupPath).toBeDefined();
148
+ });
149
+ });
150
+ (0, import_vitest.describe)("editFile", () => {
151
+ (0, import_vitest.it)("should NOT create backup when enableBackups is false (default)", async () => {
152
+ const fileSystemService = new import_filesystem_service.FileSystemService(
153
+ {
154
+ allowedPaths: [tempDir],
155
+ blockedPaths: [],
156
+ blockedExtensions: [],
157
+ maxFileSize: 10 * 1024 * 1024,
158
+ workingDirectory: tempDir,
159
+ enableBackups: false,
160
+ backupRetentionDays: 7
161
+ },
162
+ mockLogger
163
+ );
164
+ await fileSystemService.initialize();
165
+ const testFile = path.join(tempDir, "test.txt");
166
+ await fs.writeFile(testFile, "hello world");
167
+ const result = await fileSystemService.editFile(testFile, {
168
+ oldString: "world",
169
+ newString: "universe"
170
+ });
171
+ (0, import_vitest.expect)(result.success).toBe(true);
172
+ (0, import_vitest.expect)(result.backupPath).toBeUndefined();
173
+ const content = await fs.readFile(testFile, "utf-8");
174
+ (0, import_vitest.expect)(content).toBe("hello universe");
175
+ });
176
+ (0, import_vitest.it)("should create backup when enableBackups is true", async () => {
177
+ const fileSystemService = new import_filesystem_service.FileSystemService(
178
+ {
179
+ allowedPaths: [tempDir],
180
+ blockedPaths: [],
181
+ blockedExtensions: [],
182
+ maxFileSize: 10 * 1024 * 1024,
183
+ workingDirectory: tempDir,
184
+ enableBackups: true,
185
+ backupRetentionDays: 7
186
+ },
187
+ mockLogger
188
+ );
189
+ await fileSystemService.initialize();
190
+ const testFile = path.join(tempDir, "test.txt");
191
+ await fs.writeFile(testFile, "hello world");
192
+ const result = await fileSystemService.editFile(testFile, {
193
+ oldString: "world",
194
+ newString: "universe"
195
+ });
196
+ (0, import_vitest.expect)(result.success).toBe(true);
197
+ (0, import_vitest.expect)(result.backupPath).toBeDefined();
198
+ const backupContent = await fs.readFile(result.backupPath, "utf-8");
199
+ (0, import_vitest.expect)(backupContent).toBe("hello world");
200
+ const content = await fs.readFile(testFile, "utf-8");
201
+ (0, import_vitest.expect)(content).toBe("hello universe");
202
+ });
203
+ (0, import_vitest.it)("should respect per-call backup option over config", async () => {
204
+ const fileSystemService = new import_filesystem_service.FileSystemService(
205
+ {
206
+ allowedPaths: [tempDir],
207
+ blockedPaths: [],
208
+ blockedExtensions: [],
209
+ maxFileSize: 10 * 1024 * 1024,
210
+ workingDirectory: tempDir,
211
+ enableBackups: false,
212
+ // Config says no backups
213
+ backupRetentionDays: 7
214
+ },
215
+ mockLogger
216
+ );
217
+ await fileSystemService.initialize();
218
+ const testFile = path.join(tempDir, "test.txt");
219
+ await fs.writeFile(testFile, "hello world");
220
+ const result = await fileSystemService.editFile(
221
+ testFile,
222
+ {
223
+ oldString: "world",
224
+ newString: "universe"
225
+ },
226
+ { backup: true }
227
+ );
228
+ (0, import_vitest.expect)(result.success).toBe(true);
229
+ (0, import_vitest.expect)(result.backupPath).toBeDefined();
230
+ });
231
+ });
232
+ });
233
+ });
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,210 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import * as path from "node:path";
3
+ import * as fs from "node:fs/promises";
4
+ import * as os from "node:os";
5
+ import { FileSystemService } from "./filesystem-service.js";
6
+ const createMockLogger = () => ({
7
+ debug: vi.fn(),
8
+ info: vi.fn(),
9
+ warn: vi.fn(),
10
+ error: vi.fn(),
11
+ createChild: vi.fn().mockReturnThis()
12
+ });
13
+ describe("FileSystemService", () => {
14
+ let mockLogger;
15
+ let tempDir;
16
+ beforeEach(async () => {
17
+ mockLogger = createMockLogger();
18
+ const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-fs-test-"));
19
+ tempDir = await fs.realpath(rawTempDir);
20
+ vi.clearAllMocks();
21
+ });
22
+ afterEach(async () => {
23
+ try {
24
+ await fs.rm(tempDir, { recursive: true, force: true });
25
+ } catch {
26
+ }
27
+ });
28
+ describe("Backup Behavior", () => {
29
+ describe("writeFile", () => {
30
+ it("should NOT create backup when enableBackups is false (default)", async () => {
31
+ const fileSystemService = new FileSystemService(
32
+ {
33
+ allowedPaths: [tempDir],
34
+ blockedPaths: [],
35
+ blockedExtensions: [],
36
+ maxFileSize: 10 * 1024 * 1024,
37
+ workingDirectory: tempDir,
38
+ enableBackups: false,
39
+ backupRetentionDays: 7
40
+ },
41
+ mockLogger
42
+ );
43
+ await fileSystemService.initialize();
44
+ const testFile = path.join(tempDir, "test.txt");
45
+ await fs.writeFile(testFile, "original content");
46
+ const result = await fileSystemService.writeFile(testFile, "new content");
47
+ expect(result.success).toBe(true);
48
+ expect(result.backupPath).toBeUndefined();
49
+ const backupDir = path.join(tempDir, ".dexto-backups");
50
+ try {
51
+ const files = await fs.readdir(backupDir);
52
+ expect(files.length).toBe(0);
53
+ } catch {
54
+ }
55
+ });
56
+ it("should create backup when enableBackups is true", async () => {
57
+ const fileSystemService = new FileSystemService(
58
+ {
59
+ allowedPaths: [tempDir],
60
+ blockedPaths: [],
61
+ blockedExtensions: [],
62
+ maxFileSize: 10 * 1024 * 1024,
63
+ workingDirectory: tempDir,
64
+ enableBackups: true,
65
+ backupRetentionDays: 7
66
+ },
67
+ mockLogger
68
+ );
69
+ await fileSystemService.initialize();
70
+ const testFile = path.join(tempDir, "test.txt");
71
+ await fs.writeFile(testFile, "original content");
72
+ const result = await fileSystemService.writeFile(testFile, "new content");
73
+ expect(result.success).toBe(true);
74
+ expect(result.backupPath).toBeDefined();
75
+ expect(result.backupPath).toContain(".dexto");
76
+ expect(result.backupPath).toContain("backup");
77
+ const backupContent = await fs.readFile(result.backupPath, "utf-8");
78
+ expect(backupContent).toBe("original content");
79
+ const newContent = await fs.readFile(testFile, "utf-8");
80
+ expect(newContent).toBe("new content");
81
+ });
82
+ it("should NOT create backup for new files even when enableBackups is true", async () => {
83
+ const fileSystemService = new FileSystemService(
84
+ {
85
+ allowedPaths: [tempDir],
86
+ blockedPaths: [],
87
+ blockedExtensions: [],
88
+ maxFileSize: 10 * 1024 * 1024,
89
+ workingDirectory: tempDir,
90
+ enableBackups: true,
91
+ backupRetentionDays: 7
92
+ },
93
+ mockLogger
94
+ );
95
+ await fileSystemService.initialize();
96
+ const testFile = path.join(tempDir, "new-file.txt");
97
+ const result = await fileSystemService.writeFile(testFile, "content", {
98
+ createDirs: true
99
+ });
100
+ expect(result.success).toBe(true);
101
+ expect(result.backupPath).toBeUndefined();
102
+ });
103
+ it("should respect per-call backup option over config", async () => {
104
+ const fileSystemService = new FileSystemService(
105
+ {
106
+ allowedPaths: [tempDir],
107
+ blockedPaths: [],
108
+ blockedExtensions: [],
109
+ maxFileSize: 10 * 1024 * 1024,
110
+ workingDirectory: tempDir,
111
+ enableBackups: false,
112
+ // Config says no backups
113
+ backupRetentionDays: 7
114
+ },
115
+ mockLogger
116
+ );
117
+ await fileSystemService.initialize();
118
+ const testFile = path.join(tempDir, "test.txt");
119
+ await fs.writeFile(testFile, "original content");
120
+ const result = await fileSystemService.writeFile(testFile, "new content", {
121
+ backup: true
122
+ });
123
+ expect(result.success).toBe(true);
124
+ expect(result.backupPath).toBeDefined();
125
+ });
126
+ });
127
+ describe("editFile", () => {
128
+ it("should NOT create backup when enableBackups is false (default)", async () => {
129
+ const fileSystemService = new FileSystemService(
130
+ {
131
+ allowedPaths: [tempDir],
132
+ blockedPaths: [],
133
+ blockedExtensions: [],
134
+ maxFileSize: 10 * 1024 * 1024,
135
+ workingDirectory: tempDir,
136
+ enableBackups: false,
137
+ backupRetentionDays: 7
138
+ },
139
+ mockLogger
140
+ );
141
+ await fileSystemService.initialize();
142
+ const testFile = path.join(tempDir, "test.txt");
143
+ await fs.writeFile(testFile, "hello world");
144
+ const result = await fileSystemService.editFile(testFile, {
145
+ oldString: "world",
146
+ newString: "universe"
147
+ });
148
+ expect(result.success).toBe(true);
149
+ expect(result.backupPath).toBeUndefined();
150
+ const content = await fs.readFile(testFile, "utf-8");
151
+ expect(content).toBe("hello universe");
152
+ });
153
+ it("should create backup when enableBackups is true", async () => {
154
+ const fileSystemService = new FileSystemService(
155
+ {
156
+ allowedPaths: [tempDir],
157
+ blockedPaths: [],
158
+ blockedExtensions: [],
159
+ maxFileSize: 10 * 1024 * 1024,
160
+ workingDirectory: tempDir,
161
+ enableBackups: true,
162
+ backupRetentionDays: 7
163
+ },
164
+ mockLogger
165
+ );
166
+ await fileSystemService.initialize();
167
+ const testFile = path.join(tempDir, "test.txt");
168
+ await fs.writeFile(testFile, "hello world");
169
+ const result = await fileSystemService.editFile(testFile, {
170
+ oldString: "world",
171
+ newString: "universe"
172
+ });
173
+ expect(result.success).toBe(true);
174
+ expect(result.backupPath).toBeDefined();
175
+ const backupContent = await fs.readFile(result.backupPath, "utf-8");
176
+ expect(backupContent).toBe("hello world");
177
+ const content = await fs.readFile(testFile, "utf-8");
178
+ expect(content).toBe("hello universe");
179
+ });
180
+ it("should respect per-call backup option over config", async () => {
181
+ const fileSystemService = new FileSystemService(
182
+ {
183
+ allowedPaths: [tempDir],
184
+ blockedPaths: [],
185
+ blockedExtensions: [],
186
+ maxFileSize: 10 * 1024 * 1024,
187
+ workingDirectory: tempDir,
188
+ enableBackups: false,
189
+ // Config says no backups
190
+ backupRetentionDays: 7
191
+ },
192
+ mockLogger
193
+ );
194
+ await fileSystemService.initialize();
195
+ const testFile = path.join(tempDir, "test.txt");
196
+ await fs.writeFile(testFile, "hello world");
197
+ const result = await fileSystemService.editFile(
198
+ testFile,
199
+ {
200
+ oldString: "world",
201
+ newString: "universe"
202
+ },
203
+ { backup: true }
204
+ );
205
+ expect(result.success).toBe(true);
206
+ expect(result.backupPath).toBeDefined();
207
+ });
208
+ });
209
+ });
210
+ });
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,27 +17,78 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
  var glob_files_tool_exports = {};
20
30
  __export(glob_files_tool_exports, {
21
31
  createGlobFilesTool: () => createGlobFilesTool
22
32
  });
23
33
  module.exports = __toCommonJS(glob_files_tool_exports);
34
+ var path = __toESM(require("node:path"), 1);
24
35
  var import_zod = require("zod");
36
+ var import_core = require("@dexto/core");
25
37
  const GlobFilesInputSchema = import_zod.z.object({
26
38
  pattern: import_zod.z.string().describe('Glob pattern to match files (e.g., "**/*.ts", "src/**/*.js")'),
27
39
  path: import_zod.z.string().optional().describe("Base directory to search from (defaults to working directory)"),
28
40
  max_results: import_zod.z.number().int().positive().optional().default(1e3).describe("Maximum number of results to return (default: 1000)")
29
41
  }).strict();
30
- function createGlobFilesTool(fileSystemService) {
42
+ function createGlobFilesTool(options) {
43
+ const { fileSystemService, directoryApproval } = options;
44
+ let pendingApprovalSearchDir;
31
45
  return {
32
46
  id: "glob_files",
33
47
  description: "Find files matching a glob pattern. Supports standard glob syntax like **/*.js for recursive matches, *.ts for files in current directory, and src/**/*.tsx for nested paths. Returns array of file paths with metadata (size, modified date). Results are limited to allowed paths only.",
34
48
  inputSchema: GlobFilesInputSchema,
49
+ /**
50
+ * Check if this glob operation needs directory access approval.
51
+ * Returns custom approval request if the search directory is outside allowed paths.
52
+ */
53
+ getApprovalOverride: async (args) => {
54
+ const { path: searchPath } = args;
55
+ const baseDir = fileSystemService.getWorkingDirectory();
56
+ const searchDir = path.resolve(baseDir, searchPath || ".");
57
+ const isAllowed = await fileSystemService.isPathWithinConfigAllowed(searchDir);
58
+ if (isAllowed) {
59
+ return null;
60
+ }
61
+ if (directoryApproval?.isSessionApproved(searchDir)) {
62
+ return null;
63
+ }
64
+ pendingApprovalSearchDir = searchDir;
65
+ return {
66
+ type: import_core.ApprovalType.DIRECTORY_ACCESS,
67
+ metadata: {
68
+ path: searchDir,
69
+ parentDir: searchDir,
70
+ operation: "search",
71
+ toolName: "glob_files"
72
+ }
73
+ };
74
+ },
75
+ /**
76
+ * Handle approved directory access - remember the directory for session
77
+ */
78
+ onApprovalGranted: (response) => {
79
+ if (!directoryApproval || !pendingApprovalSearchDir) return;
80
+ const data = response.data;
81
+ const rememberDirectory = data?.rememberDirectory ?? false;
82
+ directoryApproval.addApproved(
83
+ pendingApprovalSearchDir,
84
+ rememberDirectory ? "session" : "once"
85
+ );
86
+ pendingApprovalSearchDir = void 0;
87
+ },
35
88
  execute: async (input, _context) => {
36
- const { pattern, path, max_results } = input;
89
+ const { pattern, path: path2, max_results } = input;
37
90
  const result = await fileSystemService.globFiles(pattern, {
38
- cwd: path,
91
+ cwd: path2,
39
92
  maxResults: max_results,
40
93
  includeMetadata: true
41
94
  });
@@ -1,5 +1,6 @@
1
1
  import { InternalTool } from '@dexto/core';
2
- import { FileSystemService } from './filesystem-service.cjs';
2
+ import { FileToolOptions } from './file-tool-types.cjs';
3
+ import './filesystem-service.cjs';
3
4
  import './types.cjs';
4
5
 
5
6
  /**
@@ -9,8 +10,8 @@ import './types.cjs';
9
10
  */
10
11
 
11
12
  /**
12
- * Create the glob_files internal tool
13
+ * Create the glob_files internal tool with directory approval support
13
14
  */
14
- declare function createGlobFilesTool(fileSystemService: FileSystemService): InternalTool;
15
+ declare function createGlobFilesTool(options: FileToolOptions): InternalTool;
15
16
 
16
17
  export { createGlobFilesTool };