@dexto/tools-filesystem 1.5.1 → 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,8 +1,18 @@
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
- import { DextoRuntimeError, ApprovalType } from "@dexto/core";
5
+ import {
6
+ DextoRuntimeError,
7
+ ApprovalType,
8
+ ToolError
9
+ } from "@dexto/core";
5
10
  import { FileSystemErrorCode } from "./error-codes.js";
11
+ const previewContentHashCache = /* @__PURE__ */ new Map();
12
+ const FILE_NOT_EXISTS_MARKER = null;
13
+ function computeContentHash(content) {
14
+ return createHash("sha256").update(content, "utf8").digest("hex");
15
+ }
6
16
  const WriteFileInputSchema = z.object({
7
17
  file_path: z.string().describe("Absolute path where the file should be written"),
8
18
  content: z.string().describe("Content to write to the file"),
@@ -34,10 +44,10 @@ function createWriteFileTool(options) {
34
44
  * Check if this write operation needs directory access approval.
35
45
  * Returns custom approval request if the file is outside allowed paths.
36
46
  */
37
- getApprovalOverride: (args) => {
47
+ getApprovalOverride: async (args) => {
38
48
  const { file_path } = args;
39
49
  if (!file_path) return null;
40
- const isAllowed = fileSystemService.isPathWithinConfigAllowed(file_path);
50
+ const isAllowed = await fileSystemService.isPathWithinConfigAllowed(file_path);
41
51
  if (isAllowed) {
42
52
  return null;
43
53
  }
@@ -72,15 +82,25 @@ function createWriteFileTool(options) {
72
82
  },
73
83
  /**
74
84
  * Generate preview for approval UI - shows diff or file creation info
85
+ * Stores content hash for change detection in execute phase.
75
86
  */
76
- generatePreview: async (input, _context) => {
87
+ generatePreview: async (input, context) => {
77
88
  const { file_path, content } = input;
78
89
  try {
79
90
  const originalFile = await fileSystemService.readFile(file_path);
80
91
  const originalContent = originalFile.content;
92
+ if (context?.toolCallId) {
93
+ previewContentHashCache.set(
94
+ context.toolCallId,
95
+ computeContentHash(originalContent)
96
+ );
97
+ }
81
98
  return generateDiffPreview(file_path, originalContent, content);
82
99
  } catch (error) {
83
100
  if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.FILE_NOT_FOUND) {
101
+ if (context?.toolCallId) {
102
+ previewContentHashCache.set(context.toolCallId, FILE_NOT_EXISTS_MARKER);
103
+ }
84
104
  const lineCount = content.split("\n").length;
85
105
  const preview = {
86
106
  type: "file",
@@ -96,24 +116,42 @@ function createWriteFileTool(options) {
96
116
  throw error;
97
117
  }
98
118
  },
99
- execute: async (input, _context) => {
119
+ execute: async (input, context) => {
100
120
  const { file_path, content, create_dirs, encoding } = input;
101
121
  let originalContent = null;
122
+ let fileExistsNow = false;
102
123
  try {
103
124
  const originalFile = await fileSystemService.readFile(file_path);
104
125
  originalContent = originalFile.content;
126
+ fileExistsNow = true;
105
127
  } catch (error) {
106
128
  if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.FILE_NOT_FOUND) {
107
129
  originalContent = null;
130
+ fileExistsNow = false;
108
131
  } else {
109
132
  throw error;
110
133
  }
111
134
  }
135
+ if (context?.toolCallId && previewContentHashCache.has(context.toolCallId)) {
136
+ const expectedHash = previewContentHashCache.get(context.toolCallId);
137
+ previewContentHashCache.delete(context.toolCallId);
138
+ if (expectedHash === FILE_NOT_EXISTS_MARKER) {
139
+ if (fileExistsNow) {
140
+ throw ToolError.fileModifiedSincePreview("write_file", file_path);
141
+ }
142
+ } else if (expectedHash !== null) {
143
+ if (!fileExistsNow) {
144
+ throw ToolError.fileModifiedSincePreview("write_file", file_path);
145
+ }
146
+ const currentHash = computeContentHash(originalContent);
147
+ if (expectedHash !== currentHash) {
148
+ throw ToolError.fileModifiedSincePreview("write_file", file_path);
149
+ }
150
+ }
151
+ }
112
152
  const result = await fileSystemService.writeFile(file_path, content, {
113
153
  createDirs: create_dirs,
114
- encoding,
115
- backup: true
116
- // Always create backup for internal tools
154
+ encoding
117
155
  });
118
156
  let _display;
119
157
  if (originalContent === null) {
@@ -0,0 +1,217 @@
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_write_file_tool = require("./write-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)("write_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-write-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 - Existing Files", () => {
69
+ (0, import_vitest.it)("should succeed when existing file is not modified between preview and execute", async () => {
70
+ const tool = (0, import_write_file_tool.createWriteFileTool)({ fileSystemService });
71
+ const testFile = path.join(tempDir, "test.txt");
72
+ await fs.writeFile(testFile, "original content");
73
+ const toolCallId = "test-call-123";
74
+ const input = {
75
+ file_path: testFile,
76
+ content: "new content"
77
+ };
78
+ const preview = await tool.generatePreview(input, { toolCallId });
79
+ (0, import_vitest.expect)(preview).toBeDefined();
80
+ (0, import_vitest.expect)(preview?.type).toBe("diff");
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("new content");
86
+ });
87
+ (0, import_vitest.it)("should fail when existing file is modified between preview and execute", async () => {
88
+ const tool = (0, import_write_file_tool.createWriteFileTool)({ fileSystemService });
89
+ const testFile = path.join(tempDir, "test.txt");
90
+ await fs.writeFile(testFile, "original content");
91
+ const toolCallId = "test-call-456";
92
+ const input = {
93
+ file_path: testFile,
94
+ content: "new content"
95
+ };
96
+ await tool.generatePreview(input, { toolCallId });
97
+ await fs.writeFile(testFile, "user modified this");
98
+ try {
99
+ await tool.execute(input, { toolCallId });
100
+ import_vitest.expect.fail("Should have thrown an error");
101
+ } catch (error) {
102
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
103
+ (0, import_vitest.expect)(error.code).toBe(
104
+ import_core.ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
105
+ );
106
+ }
107
+ const content = await fs.readFile(testFile, "utf-8");
108
+ (0, import_vitest.expect)(content).toBe("user modified this");
109
+ });
110
+ (0, import_vitest.it)("should fail when existing file is deleted between preview and execute", async () => {
111
+ const tool = (0, import_write_file_tool.createWriteFileTool)({ fileSystemService });
112
+ const testFile = path.join(tempDir, "test.txt");
113
+ await fs.writeFile(testFile, "original content");
114
+ const toolCallId = "test-call-deleted";
115
+ const input = {
116
+ file_path: testFile,
117
+ content: "new content"
118
+ };
119
+ await tool.generatePreview(input, { toolCallId });
120
+ await fs.unlink(testFile);
121
+ try {
122
+ await tool.execute(input, { toolCallId });
123
+ import_vitest.expect.fail("Should have thrown an error");
124
+ } catch (error) {
125
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
126
+ (0, import_vitest.expect)(error.code).toBe(
127
+ import_core.ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
128
+ );
129
+ }
130
+ });
131
+ });
132
+ (0, import_vitest.describe)("File Modification Detection - New Files", () => {
133
+ (0, import_vitest.it)("should succeed when creating new file that still does not exist", async () => {
134
+ const tool = (0, import_write_file_tool.createWriteFileTool)({ fileSystemService });
135
+ const testFile = path.join(tempDir, "new-file.txt");
136
+ const toolCallId = "test-call-new";
137
+ const input = {
138
+ file_path: testFile,
139
+ content: "brand new content"
140
+ };
141
+ const preview = await tool.generatePreview(input, { toolCallId });
142
+ (0, import_vitest.expect)(preview).toBeDefined();
143
+ (0, import_vitest.expect)(preview?.type).toBe("file");
144
+ (0, import_vitest.expect)(preview.operation).toBe("create");
145
+ const result = await tool.execute(input, { toolCallId });
146
+ (0, import_vitest.expect)(result.success).toBe(true);
147
+ const content = await fs.readFile(testFile, "utf-8");
148
+ (0, import_vitest.expect)(content).toBe("brand new content");
149
+ });
150
+ (0, import_vitest.it)("should fail when file is created by someone else between preview and execute", async () => {
151
+ const tool = (0, import_write_file_tool.createWriteFileTool)({ fileSystemService });
152
+ const testFile = path.join(tempDir, "race-condition.txt");
153
+ const toolCallId = "test-call-race";
154
+ const input = {
155
+ file_path: testFile,
156
+ content: "agent content"
157
+ };
158
+ const preview = await tool.generatePreview(input, { toolCallId });
159
+ (0, import_vitest.expect)(preview?.type).toBe("file");
160
+ await fs.writeFile(testFile, "someone else created this");
161
+ try {
162
+ await tool.execute(input, { toolCallId });
163
+ import_vitest.expect.fail("Should have thrown an error");
164
+ } catch (error) {
165
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
166
+ (0, import_vitest.expect)(error.code).toBe(
167
+ import_core.ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
168
+ );
169
+ }
170
+ const content = await fs.readFile(testFile, "utf-8");
171
+ (0, import_vitest.expect)(content).toBe("someone else created this");
172
+ });
173
+ });
174
+ (0, import_vitest.describe)("Cache Cleanup", () => {
175
+ (0, import_vitest.it)("should clean up hash cache after successful execution", async () => {
176
+ const tool = (0, import_write_file_tool.createWriteFileTool)({ fileSystemService });
177
+ const testFile = path.join(tempDir, "test.txt");
178
+ await fs.writeFile(testFile, "original");
179
+ const toolCallId = "test-call-cleanup";
180
+ const input = {
181
+ file_path: testFile,
182
+ content: "first write"
183
+ };
184
+ await tool.generatePreview(input, { toolCallId });
185
+ await tool.execute(input, { toolCallId });
186
+ const input2 = {
187
+ file_path: testFile,
188
+ content: "second write"
189
+ };
190
+ await tool.generatePreview(input2, { toolCallId });
191
+ const result = await tool.execute(input2, { toolCallId });
192
+ (0, import_vitest.expect)(result.success).toBe(true);
193
+ const content = await fs.readFile(testFile, "utf-8");
194
+ (0, import_vitest.expect)(content).toBe("second write");
195
+ });
196
+ (0, import_vitest.it)("should clean up hash cache after failed execution", async () => {
197
+ const tool = (0, import_write_file_tool.createWriteFileTool)({ fileSystemService });
198
+ const testFile = path.join(tempDir, "test.txt");
199
+ await fs.writeFile(testFile, "original");
200
+ const toolCallId = "test-call-fail";
201
+ const input = {
202
+ file_path: testFile,
203
+ content: "new content"
204
+ };
205
+ await tool.generatePreview(input, { toolCallId });
206
+ await fs.writeFile(testFile, "modified");
207
+ try {
208
+ await tool.execute(input, { toolCallId });
209
+ } catch {
210
+ }
211
+ await fs.writeFile(testFile, "reset content");
212
+ await tool.generatePreview(input, { toolCallId });
213
+ const result = await tool.execute(input, { toolCallId });
214
+ (0, import_vitest.expect)(result.success).toBe(true);
215
+ });
216
+ });
217
+ });
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,194 @@
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 { createWriteFileTool } from "./write-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("write_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-write-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 - Existing Files", () => {
46
+ it("should succeed when existing file is not modified between preview and execute", async () => {
47
+ const tool = createWriteFileTool({ fileSystemService });
48
+ const testFile = path.join(tempDir, "test.txt");
49
+ await fs.writeFile(testFile, "original content");
50
+ const toolCallId = "test-call-123";
51
+ const input = {
52
+ file_path: testFile,
53
+ content: "new content"
54
+ };
55
+ const preview = await tool.generatePreview(input, { toolCallId });
56
+ expect(preview).toBeDefined();
57
+ expect(preview?.type).toBe("diff");
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("new content");
63
+ });
64
+ it("should fail when existing file is modified between preview and execute", async () => {
65
+ const tool = createWriteFileTool({ fileSystemService });
66
+ const testFile = path.join(tempDir, "test.txt");
67
+ await fs.writeFile(testFile, "original content");
68
+ const toolCallId = "test-call-456";
69
+ const input = {
70
+ file_path: testFile,
71
+ content: "new content"
72
+ };
73
+ await tool.generatePreview(input, { toolCallId });
74
+ await fs.writeFile(testFile, "user modified this");
75
+ try {
76
+ await tool.execute(input, { toolCallId });
77
+ expect.fail("Should have thrown an error");
78
+ } catch (error) {
79
+ expect(error).toBeInstanceOf(DextoRuntimeError);
80
+ expect(error.code).toBe(
81
+ ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
82
+ );
83
+ }
84
+ const content = await fs.readFile(testFile, "utf-8");
85
+ expect(content).toBe("user modified this");
86
+ });
87
+ it("should fail when existing file is deleted between preview and execute", async () => {
88
+ const tool = createWriteFileTool({ fileSystemService });
89
+ const testFile = path.join(tempDir, "test.txt");
90
+ await fs.writeFile(testFile, "original content");
91
+ const toolCallId = "test-call-deleted";
92
+ const input = {
93
+ file_path: testFile,
94
+ content: "new content"
95
+ };
96
+ await tool.generatePreview(input, { toolCallId });
97
+ await fs.unlink(testFile);
98
+ try {
99
+ await tool.execute(input, { toolCallId });
100
+ expect.fail("Should have thrown an error");
101
+ } catch (error) {
102
+ expect(error).toBeInstanceOf(DextoRuntimeError);
103
+ expect(error.code).toBe(
104
+ ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
105
+ );
106
+ }
107
+ });
108
+ });
109
+ describe("File Modification Detection - New Files", () => {
110
+ it("should succeed when creating new file that still does not exist", async () => {
111
+ const tool = createWriteFileTool({ fileSystemService });
112
+ const testFile = path.join(tempDir, "new-file.txt");
113
+ const toolCallId = "test-call-new";
114
+ const input = {
115
+ file_path: testFile,
116
+ content: "brand new content"
117
+ };
118
+ const preview = await tool.generatePreview(input, { toolCallId });
119
+ expect(preview).toBeDefined();
120
+ expect(preview?.type).toBe("file");
121
+ expect(preview.operation).toBe("create");
122
+ const result = await tool.execute(input, { toolCallId });
123
+ expect(result.success).toBe(true);
124
+ const content = await fs.readFile(testFile, "utf-8");
125
+ expect(content).toBe("brand new content");
126
+ });
127
+ it("should fail when file is created by someone else between preview and execute", async () => {
128
+ const tool = createWriteFileTool({ fileSystemService });
129
+ const testFile = path.join(tempDir, "race-condition.txt");
130
+ const toolCallId = "test-call-race";
131
+ const input = {
132
+ file_path: testFile,
133
+ content: "agent content"
134
+ };
135
+ const preview = await tool.generatePreview(input, { toolCallId });
136
+ expect(preview?.type).toBe("file");
137
+ await fs.writeFile(testFile, "someone else created this");
138
+ try {
139
+ await tool.execute(input, { toolCallId });
140
+ expect.fail("Should have thrown an error");
141
+ } catch (error) {
142
+ expect(error).toBeInstanceOf(DextoRuntimeError);
143
+ expect(error.code).toBe(
144
+ ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
145
+ );
146
+ }
147
+ const content = await fs.readFile(testFile, "utf-8");
148
+ expect(content).toBe("someone else created this");
149
+ });
150
+ });
151
+ describe("Cache Cleanup", () => {
152
+ it("should clean up hash cache after successful execution", async () => {
153
+ const tool = createWriteFileTool({ fileSystemService });
154
+ const testFile = path.join(tempDir, "test.txt");
155
+ await fs.writeFile(testFile, "original");
156
+ const toolCallId = "test-call-cleanup";
157
+ const input = {
158
+ file_path: testFile,
159
+ content: "first write"
160
+ };
161
+ await tool.generatePreview(input, { toolCallId });
162
+ await tool.execute(input, { toolCallId });
163
+ const input2 = {
164
+ file_path: testFile,
165
+ content: "second write"
166
+ };
167
+ await tool.generatePreview(input2, { toolCallId });
168
+ const result = await tool.execute(input2, { toolCallId });
169
+ expect(result.success).toBe(true);
170
+ const content = await fs.readFile(testFile, "utf-8");
171
+ expect(content).toBe("second write");
172
+ });
173
+ it("should clean up hash cache after failed execution", async () => {
174
+ const tool = createWriteFileTool({ fileSystemService });
175
+ const testFile = path.join(tempDir, "test.txt");
176
+ await fs.writeFile(testFile, "original");
177
+ const toolCallId = "test-call-fail";
178
+ const input = {
179
+ file_path: testFile,
180
+ content: "new content"
181
+ };
182
+ await tool.generatePreview(input, { toolCallId });
183
+ await fs.writeFile(testFile, "modified");
184
+ try {
185
+ await tool.execute(input, { toolCallId });
186
+ } catch {
187
+ }
188
+ await fs.writeFile(testFile, "reset content");
189
+ await tool.generatePreview(input, { toolCallId });
190
+ const result = await tool.execute(input, { toolCallId });
191
+ expect(result.success).toBe(true);
192
+ });
193
+ });
194
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dexto/tools-filesystem",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "FileSystem tools provider for Dexto agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "glob": "^11.1.0",
23
23
  "safe-regex": "^2.1.1",
24
24
  "zod": "^3.25.0",
25
- "@dexto/core": "1.5.1"
25
+ "@dexto/core": "1.5.3"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/diff": "^5.2.3",