@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
@@ -1,10 +1,16 @@
1
1
  import * as path from "node:path";
2
+ import { createHash } from "node:crypto";
2
3
  import { z } from "zod";
3
4
  import { createPatch } from "diff";
4
5
  import { ApprovalType } from "@dexto/core";
5
6
  import { ToolError } from "@dexto/core";
6
7
  import { ToolErrorCode } from "@dexto/core";
7
8
  import { DextoRuntimeError } from "@dexto/core";
9
+ import { FileSystemErrorCode } from "./error-codes.js";
10
+ const previewContentHashCache = /* @__PURE__ */ new Map();
11
+ function computeContentHash(content) {
12
+ return createHash("sha256").update(content, "utf8").digest("hex");
13
+ }
8
14
  const EditFileInputSchema = z.object({
9
15
  file_path: z.string().describe("Absolute path to the file to edit"),
10
16
  old_string: z.string().describe("Text to replace (must be unique unless replace_all is true)"),
@@ -36,10 +42,10 @@ function createEditFileTool(options) {
36
42
  * Check if this edit operation needs directory access approval.
37
43
  * Returns custom approval request if the file is outside allowed paths.
38
44
  */
39
- getApprovalOverride: (args) => {
45
+ getApprovalOverride: async (args) => {
40
46
  const { file_path } = args;
41
47
  if (!file_path) return null;
42
- const isAllowed = fileSystemService.isPathWithinConfigAllowed(file_path);
48
+ const isAllowed = await fileSystemService.isPathWithinConfigAllowed(file_path);
43
49
  if (isAllowed) {
44
50
  return null;
45
51
  }
@@ -75,12 +81,19 @@ function createEditFileTool(options) {
75
81
  /**
76
82
  * Generate preview for approval UI - shows diff without modifying file
77
83
  * Throws ToolError.validationFailed() for validation errors (file not found, string not found)
84
+ * Stores content hash for change detection in execute phase.
78
85
  */
79
- generatePreview: async (input, _context) => {
86
+ generatePreview: async (input, context) => {
80
87
  const { file_path, old_string, new_string, replace_all } = input;
81
88
  try {
82
89
  const originalFile = await fileSystemService.readFile(file_path);
83
90
  const originalContent = originalFile.content;
91
+ if (context?.toolCallId) {
92
+ previewContentHashCache.set(
93
+ context.toolCallId,
94
+ computeContentHash(originalContent)
95
+ );
96
+ }
84
97
  if (!replace_all) {
85
98
  const occurrences = originalContent.split(old_string).length - 1;
86
99
  if (occurrences > 1) {
@@ -113,25 +126,36 @@ function createEditFileTool(options) {
113
126
  return null;
114
127
  }
115
128
  },
116
- execute: async (input, _context) => {
129
+ execute: async (input, context) => {
117
130
  const { file_path, old_string, new_string, replace_all } = input;
118
- const originalFile = await fileSystemService.readFile(file_path);
119
- const originalContent = originalFile.content;
120
- const result = await fileSystemService.editFile(
121
- file_path,
122
- {
123
- oldString: old_string,
124
- newString: new_string,
125
- replaceAll: replace_all
126
- },
127
- {
128
- backup: true
129
- // Always create backup for internal tools
131
+ if (context?.toolCallId && previewContentHashCache.has(context.toolCallId)) {
132
+ const expectedHash = previewContentHashCache.get(context.toolCallId);
133
+ previewContentHashCache.delete(context.toolCallId);
134
+ let currentContent;
135
+ try {
136
+ const currentFile = await fileSystemService.readFile(file_path);
137
+ currentContent = currentFile.content;
138
+ } catch (error) {
139
+ if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.FILE_NOT_FOUND) {
140
+ throw ToolError.fileModifiedSincePreview("edit_file", file_path);
141
+ }
142
+ throw error;
143
+ }
144
+ const currentHash = computeContentHash(currentContent);
145
+ if (expectedHash !== currentHash) {
146
+ throw ToolError.fileModifiedSincePreview("edit_file", file_path);
130
147
  }
148
+ }
149
+ const result = await fileSystemService.editFile(file_path, {
150
+ oldString: old_string,
151
+ newString: new_string,
152
+ replaceAll: replace_all
153
+ });
154
+ const _display = generateDiffPreview(
155
+ file_path,
156
+ result.originalContent,
157
+ result.newContent
131
158
  );
132
- const newFile = await fileSystemService.readFile(file_path);
133
- const newContent = newFile.content;
134
- const _display = generateDiffPreview(file_path, originalContent, newContent);
135
159
  return {
136
160
  success: result.success,
137
161
  path: result.path,
@@ -0,0 +1,203 @@
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_edit_file_tool = require("./edit-file-tool.js");
29
+ var import_filesystem_service = require("./filesystem-service.js");
30
+ var import_core = require("@dexto/core");
31
+ var import_core2 = require("@dexto/core");
32
+ const createMockLogger = () => ({
33
+ debug: import_vitest.vi.fn(),
34
+ info: import_vitest.vi.fn(),
35
+ warn: import_vitest.vi.fn(),
36
+ error: import_vitest.vi.fn(),
37
+ createChild: import_vitest.vi.fn().mockReturnThis()
38
+ });
39
+ (0, import_vitest.describe)("edit_file tool", () => {
40
+ let mockLogger;
41
+ let tempDir;
42
+ let fileSystemService;
43
+ (0, import_vitest.beforeEach)(async () => {
44
+ mockLogger = createMockLogger();
45
+ const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-edit-test-"));
46
+ tempDir = await fs.realpath(rawTempDir);
47
+ fileSystemService = new import_filesystem_service.FileSystemService(
48
+ {
49
+ allowedPaths: [tempDir],
50
+ blockedPaths: [],
51
+ blockedExtensions: [],
52
+ maxFileSize: 10 * 1024 * 1024,
53
+ workingDirectory: tempDir,
54
+ enableBackups: false,
55
+ backupRetentionDays: 7
56
+ },
57
+ mockLogger
58
+ );
59
+ await fileSystemService.initialize();
60
+ import_vitest.vi.clearAllMocks();
61
+ });
62
+ (0, import_vitest.afterEach)(async () => {
63
+ try {
64
+ await fs.rm(tempDir, { recursive: true, force: true });
65
+ } catch {
66
+ }
67
+ });
68
+ (0, import_vitest.describe)("File Modification Detection", () => {
69
+ (0, import_vitest.it)("should succeed when file is not modified between preview and execute", async () => {
70
+ const tool = (0, import_edit_file_tool.createEditFileTool)({ fileSystemService });
71
+ const testFile = path.join(tempDir, "test.txt");
72
+ await fs.writeFile(testFile, "hello world");
73
+ const toolCallId = "test-call-123";
74
+ const input = {
75
+ file_path: testFile,
76
+ old_string: "world",
77
+ new_string: "universe"
78
+ };
79
+ const preview = await tool.generatePreview(input, { toolCallId });
80
+ (0, import_vitest.expect)(preview).toBeDefined();
81
+ const result = await tool.execute(input, { toolCallId });
82
+ (0, import_vitest.expect)(result.success).toBe(true);
83
+ (0, import_vitest.expect)(result.path).toBe(testFile);
84
+ const content = await fs.readFile(testFile, "utf-8");
85
+ (0, import_vitest.expect)(content).toBe("hello universe");
86
+ });
87
+ (0, import_vitest.it)("should fail when file is modified between preview and execute", async () => {
88
+ const tool = (0, import_edit_file_tool.createEditFileTool)({ fileSystemService });
89
+ const testFile = path.join(tempDir, "test.txt");
90
+ await fs.writeFile(testFile, "hello world");
91
+ const toolCallId = "test-call-456";
92
+ const input = {
93
+ file_path: testFile,
94
+ old_string: "world",
95
+ new_string: "universe"
96
+ };
97
+ const preview = await tool.generatePreview(input, { toolCallId });
98
+ (0, import_vitest.expect)(preview).toBeDefined();
99
+ await fs.writeFile(testFile, "hello world - user added this");
100
+ try {
101
+ await tool.execute(input, { toolCallId });
102
+ import_vitest.expect.fail("Should have thrown an error");
103
+ } catch (error) {
104
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
105
+ (0, import_vitest.expect)(error.code).toBe(
106
+ import_core.ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
107
+ );
108
+ }
109
+ const content = await fs.readFile(testFile, "utf-8");
110
+ (0, import_vitest.expect)(content).toBe("hello world - user added this");
111
+ });
112
+ (0, import_vitest.it)("should detect file modification with correct error code", async () => {
113
+ const tool = (0, import_edit_file_tool.createEditFileTool)({ fileSystemService });
114
+ const testFile = path.join(tempDir, "test.txt");
115
+ await fs.writeFile(testFile, "hello world");
116
+ const toolCallId = "test-call-789";
117
+ const input = {
118
+ file_path: testFile,
119
+ old_string: "world",
120
+ new_string: "universe"
121
+ };
122
+ await tool.generatePreview(input, { toolCallId });
123
+ await fs.writeFile(testFile, "hello world modified");
124
+ try {
125
+ await tool.execute(input, { toolCallId });
126
+ import_vitest.expect.fail("Should have thrown an error");
127
+ } catch (error) {
128
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
129
+ (0, import_vitest.expect)(error.code).toBe(
130
+ import_core.ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
131
+ );
132
+ (0, import_vitest.expect)(error.message).toContain(
133
+ "modified since the preview"
134
+ );
135
+ (0, import_vitest.expect)(error.message).toContain("read the file again");
136
+ }
137
+ });
138
+ (0, import_vitest.it)("should work without toolCallId (no modification check)", async () => {
139
+ const tool = (0, import_edit_file_tool.createEditFileTool)({ fileSystemService });
140
+ const testFile = path.join(tempDir, "test.txt");
141
+ await fs.writeFile(testFile, "hello world");
142
+ const input = {
143
+ file_path: testFile,
144
+ old_string: "world",
145
+ new_string: "universe"
146
+ };
147
+ await tool.generatePreview(input, {});
148
+ await fs.writeFile(testFile, "hello world changed");
149
+ try {
150
+ await tool.execute(input, {});
151
+ } catch (error) {
152
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
153
+ (0, import_vitest.expect)(error.code).not.toBe(
154
+ import_core.ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
155
+ );
156
+ }
157
+ });
158
+ (0, import_vitest.it)("should clean up hash cache after successful execution", async () => {
159
+ const tool = (0, import_edit_file_tool.createEditFileTool)({ fileSystemService });
160
+ const testFile = path.join(tempDir, "test.txt");
161
+ await fs.writeFile(testFile, "hello world");
162
+ const toolCallId = "test-call-cleanup";
163
+ const input = {
164
+ file_path: testFile,
165
+ old_string: "world",
166
+ new_string: "universe"
167
+ };
168
+ await tool.generatePreview(input, { toolCallId });
169
+ await tool.execute(input, { toolCallId });
170
+ const input2 = {
171
+ file_path: testFile,
172
+ old_string: "universe",
173
+ new_string: "galaxy"
174
+ };
175
+ await tool.generatePreview(input2, { toolCallId });
176
+ const result = await tool.execute(input2, { toolCallId });
177
+ (0, import_vitest.expect)(result.success).toBe(true);
178
+ const content = await fs.readFile(testFile, "utf-8");
179
+ (0, import_vitest.expect)(content).toBe("hello galaxy");
180
+ });
181
+ (0, import_vitest.it)("should clean up hash cache after failed execution", async () => {
182
+ const tool = (0, import_edit_file_tool.createEditFileTool)({ fileSystemService });
183
+ const testFile = path.join(tempDir, "test.txt");
184
+ await fs.writeFile(testFile, "hello world");
185
+ const toolCallId = "test-call-fail-cleanup";
186
+ const input = {
187
+ file_path: testFile,
188
+ old_string: "world",
189
+ new_string: "universe"
190
+ };
191
+ await tool.generatePreview(input, { toolCallId });
192
+ await fs.writeFile(testFile, "hello world modified");
193
+ try {
194
+ await tool.execute(input, { toolCallId });
195
+ } catch {
196
+ }
197
+ await fs.writeFile(testFile, "hello world");
198
+ await tool.generatePreview(input, { toolCallId });
199
+ const result = await tool.execute(input, { toolCallId });
200
+ (0, import_vitest.expect)(result.success).toBe(true);
201
+ });
202
+ });
203
+ });
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,180 @@
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 { createEditFileTool } from "./edit-file-tool.js";
6
+ import { FileSystemService } from "./filesystem-service.js";
7
+ import { ToolErrorCode } from "@dexto/core";
8
+ import { DextoRuntimeError } from "@dexto/core";
9
+ const createMockLogger = () => ({
10
+ debug: vi.fn(),
11
+ info: vi.fn(),
12
+ warn: vi.fn(),
13
+ error: vi.fn(),
14
+ createChild: vi.fn().mockReturnThis()
15
+ });
16
+ describe("edit_file tool", () => {
17
+ let mockLogger;
18
+ let tempDir;
19
+ let fileSystemService;
20
+ beforeEach(async () => {
21
+ mockLogger = createMockLogger();
22
+ const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-edit-test-"));
23
+ tempDir = await fs.realpath(rawTempDir);
24
+ fileSystemService = new FileSystemService(
25
+ {
26
+ allowedPaths: [tempDir],
27
+ blockedPaths: [],
28
+ blockedExtensions: [],
29
+ maxFileSize: 10 * 1024 * 1024,
30
+ workingDirectory: tempDir,
31
+ enableBackups: false,
32
+ backupRetentionDays: 7
33
+ },
34
+ mockLogger
35
+ );
36
+ await fileSystemService.initialize();
37
+ vi.clearAllMocks();
38
+ });
39
+ afterEach(async () => {
40
+ try {
41
+ await fs.rm(tempDir, { recursive: true, force: true });
42
+ } catch {
43
+ }
44
+ });
45
+ describe("File Modification Detection", () => {
46
+ it("should succeed when file is not modified between preview and execute", async () => {
47
+ const tool = createEditFileTool({ fileSystemService });
48
+ const testFile = path.join(tempDir, "test.txt");
49
+ await fs.writeFile(testFile, "hello world");
50
+ const toolCallId = "test-call-123";
51
+ const input = {
52
+ file_path: testFile,
53
+ old_string: "world",
54
+ new_string: "universe"
55
+ };
56
+ const preview = await tool.generatePreview(input, { toolCallId });
57
+ expect(preview).toBeDefined();
58
+ const result = await tool.execute(input, { toolCallId });
59
+ expect(result.success).toBe(true);
60
+ expect(result.path).toBe(testFile);
61
+ const content = await fs.readFile(testFile, "utf-8");
62
+ expect(content).toBe("hello universe");
63
+ });
64
+ it("should fail when file is modified between preview and execute", async () => {
65
+ const tool = createEditFileTool({ fileSystemService });
66
+ const testFile = path.join(tempDir, "test.txt");
67
+ await fs.writeFile(testFile, "hello world");
68
+ const toolCallId = "test-call-456";
69
+ const input = {
70
+ file_path: testFile,
71
+ old_string: "world",
72
+ new_string: "universe"
73
+ };
74
+ const preview = await tool.generatePreview(input, { toolCallId });
75
+ expect(preview).toBeDefined();
76
+ await fs.writeFile(testFile, "hello world - user added this");
77
+ try {
78
+ await tool.execute(input, { toolCallId });
79
+ expect.fail("Should have thrown an error");
80
+ } catch (error) {
81
+ expect(error).toBeInstanceOf(DextoRuntimeError);
82
+ expect(error.code).toBe(
83
+ ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
84
+ );
85
+ }
86
+ const content = await fs.readFile(testFile, "utf-8");
87
+ expect(content).toBe("hello world - user added this");
88
+ });
89
+ it("should detect file modification with correct error code", async () => {
90
+ const tool = createEditFileTool({ fileSystemService });
91
+ const testFile = path.join(tempDir, "test.txt");
92
+ await fs.writeFile(testFile, "hello world");
93
+ const toolCallId = "test-call-789";
94
+ const input = {
95
+ file_path: testFile,
96
+ old_string: "world",
97
+ new_string: "universe"
98
+ };
99
+ await tool.generatePreview(input, { toolCallId });
100
+ await fs.writeFile(testFile, "hello world modified");
101
+ try {
102
+ await tool.execute(input, { toolCallId });
103
+ expect.fail("Should have thrown an error");
104
+ } catch (error) {
105
+ expect(error).toBeInstanceOf(DextoRuntimeError);
106
+ expect(error.code).toBe(
107
+ ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
108
+ );
109
+ expect(error.message).toContain(
110
+ "modified since the preview"
111
+ );
112
+ expect(error.message).toContain("read the file again");
113
+ }
114
+ });
115
+ it("should work without toolCallId (no modification check)", async () => {
116
+ const tool = createEditFileTool({ fileSystemService });
117
+ const testFile = path.join(tempDir, "test.txt");
118
+ await fs.writeFile(testFile, "hello world");
119
+ const input = {
120
+ file_path: testFile,
121
+ old_string: "world",
122
+ new_string: "universe"
123
+ };
124
+ await tool.generatePreview(input, {});
125
+ await fs.writeFile(testFile, "hello world changed");
126
+ try {
127
+ await tool.execute(input, {});
128
+ } catch (error) {
129
+ expect(error).toBeInstanceOf(DextoRuntimeError);
130
+ expect(error.code).not.toBe(
131
+ ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
132
+ );
133
+ }
134
+ });
135
+ it("should clean up hash cache after successful execution", async () => {
136
+ const tool = createEditFileTool({ fileSystemService });
137
+ const testFile = path.join(tempDir, "test.txt");
138
+ await fs.writeFile(testFile, "hello world");
139
+ const toolCallId = "test-call-cleanup";
140
+ const input = {
141
+ file_path: testFile,
142
+ old_string: "world",
143
+ new_string: "universe"
144
+ };
145
+ await tool.generatePreview(input, { toolCallId });
146
+ await tool.execute(input, { toolCallId });
147
+ const input2 = {
148
+ file_path: testFile,
149
+ old_string: "universe",
150
+ new_string: "galaxy"
151
+ };
152
+ await tool.generatePreview(input2, { toolCallId });
153
+ const result = await tool.execute(input2, { toolCallId });
154
+ expect(result.success).toBe(true);
155
+ const content = await fs.readFile(testFile, "utf-8");
156
+ expect(content).toBe("hello galaxy");
157
+ });
158
+ it("should clean up hash cache after failed execution", async () => {
159
+ const tool = createEditFileTool({ fileSystemService });
160
+ const testFile = path.join(tempDir, "test.txt");
161
+ await fs.writeFile(testFile, "hello world");
162
+ const toolCallId = "test-call-fail-cleanup";
163
+ const input = {
164
+ file_path: testFile,
165
+ old_string: "world",
166
+ new_string: "universe"
167
+ };
168
+ await tool.generatePreview(input, { toolCallId });
169
+ await fs.writeFile(testFile, "hello world modified");
170
+ try {
171
+ await tool.execute(input, { toolCallId });
172
+ } catch {
173
+ }
174
+ await fs.writeFile(testFile, "hello world");
175
+ await tool.generatePreview(input, { toolCallId });
176
+ const result = await tool.execute(input, { toolCallId });
177
+ expect(result.success).toBe(true);
178
+ });
179
+ });
180
+ });
@@ -126,7 +126,7 @@ class FileSystemService {
126
126
  * @param filePath The file path to check (can be relative or absolute)
127
127
  * @returns true if the path is within config-allowed paths, false otherwise
128
128
  */
129
- isPathWithinConfigAllowed(filePath) {
129
+ async isPathWithinConfigAllowed(filePath) {
130
130
  return this.pathValidator.isPathWithinAllowed(filePath);
131
131
  }
132
132
  /**
@@ -134,7 +134,7 @@ class FileSystemService {
134
134
  */
135
135
  async readFile(filePath, options = {}) {
136
136
  await this.ensureInitialized();
137
- const validation = this.pathValidator.validatePath(filePath);
137
+ const validation = await this.pathValidator.validatePath(filePath);
138
138
  if (!validation.isValid || !validation.normalizedPath) {
139
139
  throw import_errors.FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
140
140
  }
@@ -211,7 +211,7 @@ class FileSystemService {
211
211
  });
212
212
  const validFiles = [];
213
213
  for (const file of files) {
214
- const validation = this.pathValidator.validatePath(file);
214
+ const validation = await this.pathValidator.validatePath(file);
215
215
  if (!validation.isValid || !validation.normalizedPath) {
216
216
  this.logger.debug(`Skipping invalid path: ${file}`);
217
217
  continue;
@@ -344,7 +344,7 @@ class FileSystemService {
344
344
  */
345
345
  async writeFile(filePath, content, options = {}) {
346
346
  await this.ensureInitialized();
347
- const validation = this.pathValidator.validatePath(filePath);
347
+ const validation = await this.pathValidator.validatePath(filePath);
348
348
  if (!validation.isValid || !validation.normalizedPath) {
349
349
  throw import_errors.FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
350
350
  }
@@ -386,14 +386,14 @@ class FileSystemService {
386
386
  */
387
387
  async editFile(filePath, operation, options = {}) {
388
388
  await this.ensureInitialized();
389
- const validation = this.pathValidator.validatePath(filePath);
389
+ const validation = await this.pathValidator.validatePath(filePath);
390
390
  if (!validation.isValid || !validation.normalizedPath) {
391
391
  throw import_errors.FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
392
392
  }
393
393
  const normalizedPath = validation.normalizedPath;
394
394
  const fileContent = await this.readFile(normalizedPath);
395
- let content = fileContent.content;
396
- const occurrences = (content.match(
395
+ const originalContent = fileContent.content;
396
+ const occurrences = (originalContent.match(
397
397
  new RegExp(operation.oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g")
398
398
  ) || []).length;
399
399
  if (occurrences === 0) {
@@ -407,21 +407,24 @@ class FileSystemService {
407
407
  backupPath = await this.createBackup(normalizedPath);
408
408
  }
409
409
  try {
410
+ let newContent;
410
411
  if (operation.replaceAll) {
411
- content = content.replace(
412
+ newContent = originalContent.replace(
412
413
  new RegExp(operation.oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
413
414
  operation.newString
414
415
  );
415
416
  } else {
416
- content = content.replace(operation.oldString, operation.newString);
417
+ newContent = originalContent.replace(operation.oldString, operation.newString);
417
418
  }
418
- await fs.writeFile(normalizedPath, content, options.encoding || DEFAULT_ENCODING);
419
+ await fs.writeFile(normalizedPath, newContent, options.encoding || DEFAULT_ENCODING);
419
420
  this.logger.debug(`File edited: ${normalizedPath} (${occurrences} replacements)`);
420
421
  return {
421
422
  success: true,
422
423
  path: normalizedPath,
423
424
  changesCount: occurrences,
424
- backupPath
425
+ backupPath,
426
+ originalContent,
427
+ newContent
425
428
  };
426
429
  } catch (error) {
427
430
  throw import_errors.FileSystemError.editFailed(
@@ -513,10 +516,10 @@ class FileSystemService {
513
516
  return { ...this.config };
514
517
  }
515
518
  /**
516
- * Check if a path is allowed
519
+ * Check if a path is allowed (async for non-blocking symlink resolution)
517
520
  */
518
- isPathAllowed(filePath) {
519
- const validation = this.pathValidator.validatePath(filePath);
521
+ async isPathAllowed(filePath) {
522
+ const validation = await this.pathValidator.validatePath(filePath);
520
523
  return validation.isValid;
521
524
  }
522
525
  }
@@ -65,7 +65,7 @@ declare class FileSystemService {
65
65
  * @param filePath The file path to check (can be relative or absolute)
66
66
  * @returns true if the path is within config-allowed paths, false otherwise
67
67
  */
68
- isPathWithinConfigAllowed(filePath: string): boolean;
68
+ isPathWithinConfigAllowed(filePath: string): Promise<boolean>;
69
69
  /**
70
70
  * Read a file with validation and size limits
71
71
  */
@@ -99,9 +99,9 @@ declare class FileSystemService {
99
99
  */
100
100
  getConfig(): Readonly<FileSystemConfig>;
101
101
  /**
102
- * Check if a path is allowed
102
+ * Check if a path is allowed (async for non-blocking symlink resolution)
103
103
  */
104
- isPathAllowed(filePath: string): boolean;
104
+ isPathAllowed(filePath: string): Promise<boolean>;
105
105
  }
106
106
 
107
107
  export { FileSystemService };
@@ -65,7 +65,7 @@ declare class FileSystemService {
65
65
  * @param filePath The file path to check (can be relative or absolute)
66
66
  * @returns true if the path is within config-allowed paths, false otherwise
67
67
  */
68
- isPathWithinConfigAllowed(filePath: string): boolean;
68
+ isPathWithinConfigAllowed(filePath: string): Promise<boolean>;
69
69
  /**
70
70
  * Read a file with validation and size limits
71
71
  */
@@ -99,9 +99,9 @@ declare class FileSystemService {
99
99
  */
100
100
  getConfig(): Readonly<FileSystemConfig>;
101
101
  /**
102
- * Check if a path is allowed
102
+ * Check if a path is allowed (async for non-blocking symlink resolution)
103
103
  */
104
- isPathAllowed(filePath: string): boolean;
104
+ isPathAllowed(filePath: string): Promise<boolean>;
105
105
  }
106
106
 
107
107
  export { FileSystemService };