@dexto/tools-filesystem 1.5.8 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/directory-approval.cjs +98 -0
- package/dist/directory-approval.d.ts +24 -0
- package/dist/directory-approval.d.ts.map +1 -0
- package/dist/directory-approval.integration.test.cjs +175 -390
- package/dist/directory-approval.integration.test.d.ts +14 -2
- package/dist/directory-approval.integration.test.d.ts.map +1 -0
- package/dist/directory-approval.integration.test.js +178 -390
- package/dist/directory-approval.js +63 -0
- package/dist/edit-file-tool.cjs +109 -120
- package/dist/edit-file-tool.d.ts +22 -9
- package/dist/edit-file-tool.d.ts.map +1 -0
- package/dist/edit-file-tool.js +116 -110
- package/dist/edit-file-tool.test.cjs +109 -29
- package/dist/edit-file-tool.test.d.ts +7 -2
- package/dist/edit-file-tool.test.d.ts.map +1 -0
- package/dist/edit-file-tool.test.js +109 -29
- package/dist/error-codes.cjs +4 -0
- package/dist/error-codes.d.ts +6 -3
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +4 -0
- package/dist/errors.cjs +48 -0
- package/dist/errors.d.ts +20 -7
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +48 -0
- package/dist/file-tool-types.d.ts +8 -40
- package/dist/file-tool-types.d.ts.map +1 -0
- package/dist/filesystem-service.cjs +325 -10
- package/dist/filesystem-service.d.ts +41 -12
- package/dist/filesystem-service.d.ts.map +1 -0
- package/dist/filesystem-service.js +326 -11
- package/dist/filesystem-service.test.cjs +10 -2
- package/dist/filesystem-service.test.d.ts +7 -2
- package/dist/filesystem-service.test.d.ts.map +1 -0
- package/dist/filesystem-service.test.js +10 -2
- package/dist/glob-files-tool.cjs +32 -46
- package/dist/glob-files-tool.d.ts +19 -9
- package/dist/glob-files-tool.d.ts.map +1 -0
- package/dist/glob-files-tool.js +33 -47
- package/dist/grep-content-tool.cjs +40 -45
- package/dist/grep-content-tool.d.ts +28 -9
- package/dist/grep-content-tool.d.ts.map +1 -0
- package/dist/grep-content-tool.js +41 -46
- package/dist/index.cjs +6 -3
- package/dist/index.d.cts +852 -14
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -2
- package/dist/path-validator.cjs +28 -2
- package/dist/path-validator.d.ts +20 -9
- package/dist/path-validator.d.ts.map +1 -0
- package/dist/path-validator.js +28 -2
- package/dist/path-validator.test.d.ts +7 -2
- package/dist/path-validator.test.d.ts.map +1 -0
- package/dist/read-file-tool.cjs +26 -59
- package/dist/read-file-tool.d.ts +19 -9
- package/dist/read-file-tool.d.ts.map +1 -0
- package/dist/read-file-tool.js +27 -50
- package/dist/tool-factory-config.cjs +61 -0
- package/dist/{tool-provider.d.ts → tool-factory-config.d.ts} +13 -30
- package/dist/tool-factory-config.d.ts.map +1 -0
- package/dist/tool-factory-config.js +36 -0
- package/dist/tool-factory.cjs +123 -0
- package/dist/tool-factory.d.ts +4 -0
- package/dist/tool-factory.d.ts.map +1 -0
- package/dist/tool-factory.js +102 -0
- package/dist/types.d.ts +82 -18
- package/dist/types.d.ts.map +1 -0
- package/dist/write-file-tool.cjs +93 -99
- package/dist/write-file-tool.d.ts +22 -9
- package/dist/write-file-tool.d.ts.map +1 -0
- package/dist/write-file-tool.js +97 -91
- package/dist/write-file-tool.test.cjs +139 -33
- package/dist/write-file-tool.test.d.ts +7 -2
- package/dist/write-file-tool.test.d.ts.map +1 -0
- package/dist/write-file-tool.test.js +139 -33
- package/package.json +5 -4
- package/dist/directory-approval.integration.test.d.cts +0 -2
- package/dist/edit-file-tool.d.cts +0 -17
- package/dist/edit-file-tool.test.d.cts +0 -2
- package/dist/error-codes.d.cts +0 -32
- package/dist/errors.d.cts +0 -112
- package/dist/file-tool-types.d.cts +0 -46
- package/dist/filesystem-service.d.cts +0 -112
- package/dist/filesystem-service.test.d.cts +0 -2
- package/dist/glob-files-tool.d.cts +0 -17
- package/dist/grep-content-tool.d.cts +0 -17
- package/dist/path-validator.d.cts +0 -97
- package/dist/path-validator.test.d.cts +0 -2
- package/dist/read-file-tool.d.cts +0 -17
- package/dist/tool-provider.cjs +0 -123
- package/dist/tool-provider.d.cts +0 -77
- package/dist/tool-provider.js +0 -99
- package/dist/types.d.cts +0 -178
- package/dist/write-file-tool.d.cts +0 -17
- package/dist/write-file-tool.test.d.cts +0 -2
|
@@ -29,13 +29,26 @@ var import_write_file_tool = require("./write-file-tool.js");
|
|
|
29
29
|
var import_filesystem_service = require("./filesystem-service.js");
|
|
30
30
|
var import_core = require("@dexto/core");
|
|
31
31
|
var import_core2 = require("@dexto/core");
|
|
32
|
-
const createMockLogger = () =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
const createMockLogger = () => {
|
|
33
|
+
const logger = {
|
|
34
|
+
debug: import_vitest.vi.fn(),
|
|
35
|
+
silly: import_vitest.vi.fn(),
|
|
36
|
+
info: import_vitest.vi.fn(),
|
|
37
|
+
warn: import_vitest.vi.fn(),
|
|
38
|
+
error: import_vitest.vi.fn(),
|
|
39
|
+
trackException: import_vitest.vi.fn(),
|
|
40
|
+
createChild: import_vitest.vi.fn(() => logger),
|
|
41
|
+
createFileOnlyChild: import_vitest.vi.fn(() => logger),
|
|
42
|
+
setLevel: import_vitest.vi.fn(),
|
|
43
|
+
getLevel: import_vitest.vi.fn(() => "debug"),
|
|
44
|
+
getLogFilePath: import_vitest.vi.fn(() => null),
|
|
45
|
+
destroy: import_vitest.vi.fn(async () => void 0)
|
|
46
|
+
};
|
|
47
|
+
return logger;
|
|
48
|
+
};
|
|
49
|
+
function createToolContext(logger, overrides = {}) {
|
|
50
|
+
return { logger, ...overrides };
|
|
51
|
+
}
|
|
39
52
|
(0, import_vitest.describe)("write_file tool", () => {
|
|
40
53
|
let mockLogger;
|
|
41
54
|
let tempDir;
|
|
@@ -66,8 +79,38 @@ const createMockLogger = () => ({
|
|
|
66
79
|
}
|
|
67
80
|
});
|
|
68
81
|
(0, import_vitest.describe)("File Modification Detection - Existing Files", () => {
|
|
82
|
+
(0, import_vitest.it)("should generate preview for existing files outside config-allowed roots (preview read only)", async () => {
|
|
83
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)(async () => fileSystemService);
|
|
84
|
+
const rawExternalDir = await fs.mkdtemp(
|
|
85
|
+
path.join(os.tmpdir(), "dexto-write-outside-allowed-")
|
|
86
|
+
);
|
|
87
|
+
const externalDir = await fs.realpath(rawExternalDir);
|
|
88
|
+
const externalFile = path.join(externalDir, "external.txt");
|
|
89
|
+
try {
|
|
90
|
+
await fs.writeFile(externalFile, "original content");
|
|
91
|
+
const toolCallId = "preview-outside-roots";
|
|
92
|
+
const parsedInput = tool.inputSchema.parse({
|
|
93
|
+
file_path: externalFile,
|
|
94
|
+
content: "new content"
|
|
95
|
+
});
|
|
96
|
+
const preview = await tool.presentation.preview(
|
|
97
|
+
parsedInput,
|
|
98
|
+
createToolContext(mockLogger, { toolCallId })
|
|
99
|
+
);
|
|
100
|
+
(0, import_vitest.expect)(preview).toBeDefined();
|
|
101
|
+
(0, import_vitest.expect)(preview?.type).toBe("diff");
|
|
102
|
+
if (preview?.type === "diff") {
|
|
103
|
+
(0, import_vitest.expect)(preview.title).toBe("Update file");
|
|
104
|
+
(0, import_vitest.expect)(preview.filename).toBe(externalFile);
|
|
105
|
+
} else {
|
|
106
|
+
import_vitest.expect.fail("Expected diff preview");
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
await fs.rm(externalDir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
69
112
|
(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)(
|
|
113
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)(async () => fileSystemService);
|
|
71
114
|
const testFile = path.join(tempDir, "test.txt");
|
|
72
115
|
await fs.writeFile(testFile, "original content");
|
|
73
116
|
const toolCallId = "test-call-123";
|
|
@@ -75,17 +118,29 @@ const createMockLogger = () => ({
|
|
|
75
118
|
file_path: testFile,
|
|
76
119
|
content: "new content"
|
|
77
120
|
};
|
|
78
|
-
const
|
|
121
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
122
|
+
const preview = await tool.presentation.preview(
|
|
123
|
+
parsedInput,
|
|
124
|
+
createToolContext(mockLogger, { toolCallId })
|
|
125
|
+
);
|
|
79
126
|
(0, import_vitest.expect)(preview).toBeDefined();
|
|
80
127
|
(0, import_vitest.expect)(preview?.type).toBe("diff");
|
|
81
|
-
|
|
128
|
+
if (preview?.type === "diff") {
|
|
129
|
+
(0, import_vitest.expect)(preview.title).toBe("Update file");
|
|
130
|
+
} else {
|
|
131
|
+
import_vitest.expect.fail("Expected diff preview");
|
|
132
|
+
}
|
|
133
|
+
const result = await tool.execute(
|
|
134
|
+
parsedInput,
|
|
135
|
+
createToolContext(mockLogger, { toolCallId })
|
|
136
|
+
);
|
|
82
137
|
(0, import_vitest.expect)(result.success).toBe(true);
|
|
83
138
|
(0, import_vitest.expect)(result.path).toBe(testFile);
|
|
84
139
|
const content = await fs.readFile(testFile, "utf-8");
|
|
85
140
|
(0, import_vitest.expect)(content).toBe("new content");
|
|
86
141
|
});
|
|
87
142
|
(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)(
|
|
143
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)(async () => fileSystemService);
|
|
89
144
|
const testFile = path.join(tempDir, "test.txt");
|
|
90
145
|
await fs.writeFile(testFile, "original content");
|
|
91
146
|
const toolCallId = "test-call-456";
|
|
@@ -93,10 +148,14 @@ const createMockLogger = () => ({
|
|
|
93
148
|
file_path: testFile,
|
|
94
149
|
content: "new content"
|
|
95
150
|
};
|
|
96
|
-
|
|
151
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
152
|
+
await tool.presentation.preview(
|
|
153
|
+
parsedInput,
|
|
154
|
+
createToolContext(mockLogger, { toolCallId })
|
|
155
|
+
);
|
|
97
156
|
await fs.writeFile(testFile, "user modified this");
|
|
98
157
|
try {
|
|
99
|
-
await tool.execute(
|
|
158
|
+
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
100
159
|
import_vitest.expect.fail("Should have thrown an error");
|
|
101
160
|
} catch (error) {
|
|
102
161
|
(0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
|
|
@@ -108,7 +167,7 @@ const createMockLogger = () => ({
|
|
|
108
167
|
(0, import_vitest.expect)(content).toBe("user modified this");
|
|
109
168
|
});
|
|
110
169
|
(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)(
|
|
170
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)(async () => fileSystemService);
|
|
112
171
|
const testFile = path.join(tempDir, "test.txt");
|
|
113
172
|
await fs.writeFile(testFile, "original content");
|
|
114
173
|
const toolCallId = "test-call-deleted";
|
|
@@ -116,10 +175,14 @@ const createMockLogger = () => ({
|
|
|
116
175
|
file_path: testFile,
|
|
117
176
|
content: "new content"
|
|
118
177
|
};
|
|
119
|
-
|
|
178
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
179
|
+
await tool.presentation.preview(
|
|
180
|
+
parsedInput,
|
|
181
|
+
createToolContext(mockLogger, { toolCallId })
|
|
182
|
+
);
|
|
120
183
|
await fs.unlink(testFile);
|
|
121
184
|
try {
|
|
122
|
-
await tool.execute(
|
|
185
|
+
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
123
186
|
import_vitest.expect.fail("Should have thrown an error");
|
|
124
187
|
} catch (error) {
|
|
125
188
|
(0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
|
|
@@ -131,35 +194,62 @@ const createMockLogger = () => ({
|
|
|
131
194
|
});
|
|
132
195
|
(0, import_vitest.describe)("File Modification Detection - New Files", () => {
|
|
133
196
|
(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)(
|
|
197
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)(async () => fileSystemService);
|
|
135
198
|
const testFile = path.join(tempDir, "new-file.txt");
|
|
136
199
|
const toolCallId = "test-call-new";
|
|
137
200
|
const input = {
|
|
138
201
|
file_path: testFile,
|
|
139
202
|
content: "brand new content"
|
|
140
203
|
};
|
|
141
|
-
const
|
|
204
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
205
|
+
const preview = await tool.presentation.preview(
|
|
206
|
+
parsedInput,
|
|
207
|
+
createToolContext(mockLogger, { toolCallId })
|
|
208
|
+
);
|
|
142
209
|
(0, import_vitest.expect)(preview).toBeDefined();
|
|
143
210
|
(0, import_vitest.expect)(preview?.type).toBe("file");
|
|
144
|
-
|
|
145
|
-
|
|
211
|
+
if (preview?.type === "file") {
|
|
212
|
+
(0, import_vitest.expect)(preview.operation).toBe("create");
|
|
213
|
+
(0, import_vitest.expect)(preview.title).toBe("Create file");
|
|
214
|
+
} else {
|
|
215
|
+
import_vitest.expect.fail("Expected file preview");
|
|
216
|
+
}
|
|
217
|
+
const result = await tool.execute(
|
|
218
|
+
parsedInput,
|
|
219
|
+
createToolContext(mockLogger, { toolCallId })
|
|
220
|
+
);
|
|
146
221
|
(0, import_vitest.expect)(result.success).toBe(true);
|
|
222
|
+
const display = result._display;
|
|
223
|
+
if (display && typeof display === "object" && "type" in display) {
|
|
224
|
+
(0, import_vitest.expect)(display.type).toBe("file");
|
|
225
|
+
const fileDisplay = display;
|
|
226
|
+
(0, import_vitest.expect)(fileDisplay.title).toBe("Create file");
|
|
227
|
+
(0, import_vitest.expect)(fileDisplay.content).toBe("brand new content");
|
|
228
|
+
} else {
|
|
229
|
+
import_vitest.expect.fail("Expected result._display");
|
|
230
|
+
}
|
|
147
231
|
const content = await fs.readFile(testFile, "utf-8");
|
|
148
232
|
(0, import_vitest.expect)(content).toBe("brand new content");
|
|
149
233
|
});
|
|
150
234
|
(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)(
|
|
235
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)(async () => fileSystemService);
|
|
236
|
+
const previewFn = tool.presentation?.preview;
|
|
237
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
152
238
|
const testFile = path.join(tempDir, "race-condition.txt");
|
|
153
239
|
const toolCallId = "test-call-race";
|
|
154
240
|
const input = {
|
|
155
241
|
file_path: testFile,
|
|
156
242
|
content: "agent content"
|
|
157
243
|
};
|
|
158
|
-
const
|
|
244
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
245
|
+
const preview = await previewFn(
|
|
246
|
+
parsedInput,
|
|
247
|
+
createToolContext(mockLogger, { toolCallId })
|
|
248
|
+
);
|
|
159
249
|
(0, import_vitest.expect)(preview?.type).toBe("file");
|
|
160
250
|
await fs.writeFile(testFile, "someone else created this");
|
|
161
251
|
try {
|
|
162
|
-
await tool.execute(
|
|
252
|
+
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
163
253
|
import_vitest.expect.fail("Should have thrown an error");
|
|
164
254
|
} catch (error) {
|
|
165
255
|
(0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
|
|
@@ -173,7 +263,7 @@ const createMockLogger = () => ({
|
|
|
173
263
|
});
|
|
174
264
|
(0, import_vitest.describe)("Cache Cleanup", () => {
|
|
175
265
|
(0, import_vitest.it)("should clean up hash cache after successful execution", async () => {
|
|
176
|
-
const tool = (0, import_write_file_tool.createWriteFileTool)(
|
|
266
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)(async () => fileSystemService);
|
|
177
267
|
const testFile = path.join(tempDir, "test.txt");
|
|
178
268
|
await fs.writeFile(testFile, "original");
|
|
179
269
|
const toolCallId = "test-call-cleanup";
|
|
@@ -181,20 +271,30 @@ const createMockLogger = () => ({
|
|
|
181
271
|
file_path: testFile,
|
|
182
272
|
content: "first write"
|
|
183
273
|
};
|
|
184
|
-
|
|
185
|
-
await tool.
|
|
274
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
275
|
+
await tool.presentation.preview(
|
|
276
|
+
parsedInput,
|
|
277
|
+
createToolContext(mockLogger, { toolCallId })
|
|
278
|
+
);
|
|
279
|
+
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
186
280
|
const input2 = {
|
|
187
281
|
file_path: testFile,
|
|
188
282
|
content: "second write"
|
|
189
283
|
};
|
|
190
|
-
|
|
191
|
-
const
|
|
284
|
+
const parsedInput2 = tool.inputSchema.parse(input2);
|
|
285
|
+
const previewFn2 = tool.presentation?.preview;
|
|
286
|
+
(0, import_vitest.expect)(previewFn2).toBeDefined();
|
|
287
|
+
await previewFn2(parsedInput2, createToolContext(mockLogger, { toolCallId }));
|
|
288
|
+
const result = await tool.execute(
|
|
289
|
+
parsedInput2,
|
|
290
|
+
createToolContext(mockLogger, { toolCallId })
|
|
291
|
+
);
|
|
192
292
|
(0, import_vitest.expect)(result.success).toBe(true);
|
|
193
293
|
const content = await fs.readFile(testFile, "utf-8");
|
|
194
294
|
(0, import_vitest.expect)(content).toBe("second write");
|
|
195
295
|
});
|
|
196
296
|
(0, import_vitest.it)("should clean up hash cache after failed execution", async () => {
|
|
197
|
-
const tool = (0, import_write_file_tool.createWriteFileTool)(
|
|
297
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)(async () => fileSystemService);
|
|
198
298
|
const testFile = path.join(tempDir, "test.txt");
|
|
199
299
|
await fs.writeFile(testFile, "original");
|
|
200
300
|
const toolCallId = "test-call-fail";
|
|
@@ -202,15 +302,21 @@ const createMockLogger = () => ({
|
|
|
202
302
|
file_path: testFile,
|
|
203
303
|
content: "new content"
|
|
204
304
|
};
|
|
205
|
-
|
|
305
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
306
|
+
const previewFn = tool.presentation?.preview;
|
|
307
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
308
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
206
309
|
await fs.writeFile(testFile, "modified");
|
|
207
310
|
try {
|
|
208
|
-
await tool.execute(
|
|
311
|
+
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
209
312
|
} catch {
|
|
210
313
|
}
|
|
211
314
|
await fs.writeFile(testFile, "reset content");
|
|
212
|
-
await
|
|
213
|
-
const result = await tool.execute(
|
|
315
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
316
|
+
const result = await tool.execute(
|
|
317
|
+
parsedInput,
|
|
318
|
+
createToolContext(mockLogger, { toolCallId })
|
|
319
|
+
);
|
|
214
320
|
(0, import_vitest.expect)(result.success).toBe(true);
|
|
215
321
|
});
|
|
216
322
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-file-tool.test.d.ts","sourceRoot":"","sources":["../src/write-file-tool.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -6,13 +6,26 @@ import { createWriteFileTool } from "./write-file-tool.js";
|
|
|
6
6
|
import { FileSystemService } from "./filesystem-service.js";
|
|
7
7
|
import { ToolErrorCode } from "@dexto/core";
|
|
8
8
|
import { DextoRuntimeError } from "@dexto/core";
|
|
9
|
-
const createMockLogger = () =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
const createMockLogger = () => {
|
|
10
|
+
const logger = {
|
|
11
|
+
debug: vi.fn(),
|
|
12
|
+
silly: vi.fn(),
|
|
13
|
+
info: vi.fn(),
|
|
14
|
+
warn: vi.fn(),
|
|
15
|
+
error: vi.fn(),
|
|
16
|
+
trackException: vi.fn(),
|
|
17
|
+
createChild: vi.fn(() => logger),
|
|
18
|
+
createFileOnlyChild: vi.fn(() => logger),
|
|
19
|
+
setLevel: vi.fn(),
|
|
20
|
+
getLevel: vi.fn(() => "debug"),
|
|
21
|
+
getLogFilePath: vi.fn(() => null),
|
|
22
|
+
destroy: vi.fn(async () => void 0)
|
|
23
|
+
};
|
|
24
|
+
return logger;
|
|
25
|
+
};
|
|
26
|
+
function createToolContext(logger, overrides = {}) {
|
|
27
|
+
return { logger, ...overrides };
|
|
28
|
+
}
|
|
16
29
|
describe("write_file tool", () => {
|
|
17
30
|
let mockLogger;
|
|
18
31
|
let tempDir;
|
|
@@ -43,8 +56,38 @@ describe("write_file tool", () => {
|
|
|
43
56
|
}
|
|
44
57
|
});
|
|
45
58
|
describe("File Modification Detection - Existing Files", () => {
|
|
59
|
+
it("should generate preview for existing files outside config-allowed roots (preview read only)", async () => {
|
|
60
|
+
const tool = createWriteFileTool(async () => fileSystemService);
|
|
61
|
+
const rawExternalDir = await fs.mkdtemp(
|
|
62
|
+
path.join(os.tmpdir(), "dexto-write-outside-allowed-")
|
|
63
|
+
);
|
|
64
|
+
const externalDir = await fs.realpath(rawExternalDir);
|
|
65
|
+
const externalFile = path.join(externalDir, "external.txt");
|
|
66
|
+
try {
|
|
67
|
+
await fs.writeFile(externalFile, "original content");
|
|
68
|
+
const toolCallId = "preview-outside-roots";
|
|
69
|
+
const parsedInput = tool.inputSchema.parse({
|
|
70
|
+
file_path: externalFile,
|
|
71
|
+
content: "new content"
|
|
72
|
+
});
|
|
73
|
+
const preview = await tool.presentation.preview(
|
|
74
|
+
parsedInput,
|
|
75
|
+
createToolContext(mockLogger, { toolCallId })
|
|
76
|
+
);
|
|
77
|
+
expect(preview).toBeDefined();
|
|
78
|
+
expect(preview?.type).toBe("diff");
|
|
79
|
+
if (preview?.type === "diff") {
|
|
80
|
+
expect(preview.title).toBe("Update file");
|
|
81
|
+
expect(preview.filename).toBe(externalFile);
|
|
82
|
+
} else {
|
|
83
|
+
expect.fail("Expected diff preview");
|
|
84
|
+
}
|
|
85
|
+
} finally {
|
|
86
|
+
await fs.rm(externalDir, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
46
89
|
it("should succeed when existing file is not modified between preview and execute", async () => {
|
|
47
|
-
const tool = createWriteFileTool(
|
|
90
|
+
const tool = createWriteFileTool(async () => fileSystemService);
|
|
48
91
|
const testFile = path.join(tempDir, "test.txt");
|
|
49
92
|
await fs.writeFile(testFile, "original content");
|
|
50
93
|
const toolCallId = "test-call-123";
|
|
@@ -52,17 +95,29 @@ describe("write_file tool", () => {
|
|
|
52
95
|
file_path: testFile,
|
|
53
96
|
content: "new content"
|
|
54
97
|
};
|
|
55
|
-
const
|
|
98
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
99
|
+
const preview = await tool.presentation.preview(
|
|
100
|
+
parsedInput,
|
|
101
|
+
createToolContext(mockLogger, { toolCallId })
|
|
102
|
+
);
|
|
56
103
|
expect(preview).toBeDefined();
|
|
57
104
|
expect(preview?.type).toBe("diff");
|
|
58
|
-
|
|
105
|
+
if (preview?.type === "diff") {
|
|
106
|
+
expect(preview.title).toBe("Update file");
|
|
107
|
+
} else {
|
|
108
|
+
expect.fail("Expected diff preview");
|
|
109
|
+
}
|
|
110
|
+
const result = await tool.execute(
|
|
111
|
+
parsedInput,
|
|
112
|
+
createToolContext(mockLogger, { toolCallId })
|
|
113
|
+
);
|
|
59
114
|
expect(result.success).toBe(true);
|
|
60
115
|
expect(result.path).toBe(testFile);
|
|
61
116
|
const content = await fs.readFile(testFile, "utf-8");
|
|
62
117
|
expect(content).toBe("new content");
|
|
63
118
|
});
|
|
64
119
|
it("should fail when existing file is modified between preview and execute", async () => {
|
|
65
|
-
const tool = createWriteFileTool(
|
|
120
|
+
const tool = createWriteFileTool(async () => fileSystemService);
|
|
66
121
|
const testFile = path.join(tempDir, "test.txt");
|
|
67
122
|
await fs.writeFile(testFile, "original content");
|
|
68
123
|
const toolCallId = "test-call-456";
|
|
@@ -70,10 +125,14 @@ describe("write_file tool", () => {
|
|
|
70
125
|
file_path: testFile,
|
|
71
126
|
content: "new content"
|
|
72
127
|
};
|
|
73
|
-
|
|
128
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
129
|
+
await tool.presentation.preview(
|
|
130
|
+
parsedInput,
|
|
131
|
+
createToolContext(mockLogger, { toolCallId })
|
|
132
|
+
);
|
|
74
133
|
await fs.writeFile(testFile, "user modified this");
|
|
75
134
|
try {
|
|
76
|
-
await tool.execute(
|
|
135
|
+
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
77
136
|
expect.fail("Should have thrown an error");
|
|
78
137
|
} catch (error) {
|
|
79
138
|
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
@@ -85,7 +144,7 @@ describe("write_file tool", () => {
|
|
|
85
144
|
expect(content).toBe("user modified this");
|
|
86
145
|
});
|
|
87
146
|
it("should fail when existing file is deleted between preview and execute", async () => {
|
|
88
|
-
const tool = createWriteFileTool(
|
|
147
|
+
const tool = createWriteFileTool(async () => fileSystemService);
|
|
89
148
|
const testFile = path.join(tempDir, "test.txt");
|
|
90
149
|
await fs.writeFile(testFile, "original content");
|
|
91
150
|
const toolCallId = "test-call-deleted";
|
|
@@ -93,10 +152,14 @@ describe("write_file tool", () => {
|
|
|
93
152
|
file_path: testFile,
|
|
94
153
|
content: "new content"
|
|
95
154
|
};
|
|
96
|
-
|
|
155
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
156
|
+
await tool.presentation.preview(
|
|
157
|
+
parsedInput,
|
|
158
|
+
createToolContext(mockLogger, { toolCallId })
|
|
159
|
+
);
|
|
97
160
|
await fs.unlink(testFile);
|
|
98
161
|
try {
|
|
99
|
-
await tool.execute(
|
|
162
|
+
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
100
163
|
expect.fail("Should have thrown an error");
|
|
101
164
|
} catch (error) {
|
|
102
165
|
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
@@ -108,35 +171,62 @@ describe("write_file tool", () => {
|
|
|
108
171
|
});
|
|
109
172
|
describe("File Modification Detection - New Files", () => {
|
|
110
173
|
it("should succeed when creating new file that still does not exist", async () => {
|
|
111
|
-
const tool = createWriteFileTool(
|
|
174
|
+
const tool = createWriteFileTool(async () => fileSystemService);
|
|
112
175
|
const testFile = path.join(tempDir, "new-file.txt");
|
|
113
176
|
const toolCallId = "test-call-new";
|
|
114
177
|
const input = {
|
|
115
178
|
file_path: testFile,
|
|
116
179
|
content: "brand new content"
|
|
117
180
|
};
|
|
118
|
-
const
|
|
181
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
182
|
+
const preview = await tool.presentation.preview(
|
|
183
|
+
parsedInput,
|
|
184
|
+
createToolContext(mockLogger, { toolCallId })
|
|
185
|
+
);
|
|
119
186
|
expect(preview).toBeDefined();
|
|
120
187
|
expect(preview?.type).toBe("file");
|
|
121
|
-
|
|
122
|
-
|
|
188
|
+
if (preview?.type === "file") {
|
|
189
|
+
expect(preview.operation).toBe("create");
|
|
190
|
+
expect(preview.title).toBe("Create file");
|
|
191
|
+
} else {
|
|
192
|
+
expect.fail("Expected file preview");
|
|
193
|
+
}
|
|
194
|
+
const result = await tool.execute(
|
|
195
|
+
parsedInput,
|
|
196
|
+
createToolContext(mockLogger, { toolCallId })
|
|
197
|
+
);
|
|
123
198
|
expect(result.success).toBe(true);
|
|
199
|
+
const display = result._display;
|
|
200
|
+
if (display && typeof display === "object" && "type" in display) {
|
|
201
|
+
expect(display.type).toBe("file");
|
|
202
|
+
const fileDisplay = display;
|
|
203
|
+
expect(fileDisplay.title).toBe("Create file");
|
|
204
|
+
expect(fileDisplay.content).toBe("brand new content");
|
|
205
|
+
} else {
|
|
206
|
+
expect.fail("Expected result._display");
|
|
207
|
+
}
|
|
124
208
|
const content = await fs.readFile(testFile, "utf-8");
|
|
125
209
|
expect(content).toBe("brand new content");
|
|
126
210
|
});
|
|
127
211
|
it("should fail when file is created by someone else between preview and execute", async () => {
|
|
128
|
-
const tool = createWriteFileTool(
|
|
212
|
+
const tool = createWriteFileTool(async () => fileSystemService);
|
|
213
|
+
const previewFn = tool.presentation?.preview;
|
|
214
|
+
expect(previewFn).toBeDefined();
|
|
129
215
|
const testFile = path.join(tempDir, "race-condition.txt");
|
|
130
216
|
const toolCallId = "test-call-race";
|
|
131
217
|
const input = {
|
|
132
218
|
file_path: testFile,
|
|
133
219
|
content: "agent content"
|
|
134
220
|
};
|
|
135
|
-
const
|
|
221
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
222
|
+
const preview = await previewFn(
|
|
223
|
+
parsedInput,
|
|
224
|
+
createToolContext(mockLogger, { toolCallId })
|
|
225
|
+
);
|
|
136
226
|
expect(preview?.type).toBe("file");
|
|
137
227
|
await fs.writeFile(testFile, "someone else created this");
|
|
138
228
|
try {
|
|
139
|
-
await tool.execute(
|
|
229
|
+
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
140
230
|
expect.fail("Should have thrown an error");
|
|
141
231
|
} catch (error) {
|
|
142
232
|
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
@@ -150,7 +240,7 @@ describe("write_file tool", () => {
|
|
|
150
240
|
});
|
|
151
241
|
describe("Cache Cleanup", () => {
|
|
152
242
|
it("should clean up hash cache after successful execution", async () => {
|
|
153
|
-
const tool = createWriteFileTool(
|
|
243
|
+
const tool = createWriteFileTool(async () => fileSystemService);
|
|
154
244
|
const testFile = path.join(tempDir, "test.txt");
|
|
155
245
|
await fs.writeFile(testFile, "original");
|
|
156
246
|
const toolCallId = "test-call-cleanup";
|
|
@@ -158,20 +248,30 @@ describe("write_file tool", () => {
|
|
|
158
248
|
file_path: testFile,
|
|
159
249
|
content: "first write"
|
|
160
250
|
};
|
|
161
|
-
|
|
162
|
-
await tool.
|
|
251
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
252
|
+
await tool.presentation.preview(
|
|
253
|
+
parsedInput,
|
|
254
|
+
createToolContext(mockLogger, { toolCallId })
|
|
255
|
+
);
|
|
256
|
+
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
163
257
|
const input2 = {
|
|
164
258
|
file_path: testFile,
|
|
165
259
|
content: "second write"
|
|
166
260
|
};
|
|
167
|
-
|
|
168
|
-
const
|
|
261
|
+
const parsedInput2 = tool.inputSchema.parse(input2);
|
|
262
|
+
const previewFn2 = tool.presentation?.preview;
|
|
263
|
+
expect(previewFn2).toBeDefined();
|
|
264
|
+
await previewFn2(parsedInput2, createToolContext(mockLogger, { toolCallId }));
|
|
265
|
+
const result = await tool.execute(
|
|
266
|
+
parsedInput2,
|
|
267
|
+
createToolContext(mockLogger, { toolCallId })
|
|
268
|
+
);
|
|
169
269
|
expect(result.success).toBe(true);
|
|
170
270
|
const content = await fs.readFile(testFile, "utf-8");
|
|
171
271
|
expect(content).toBe("second write");
|
|
172
272
|
});
|
|
173
273
|
it("should clean up hash cache after failed execution", async () => {
|
|
174
|
-
const tool = createWriteFileTool(
|
|
274
|
+
const tool = createWriteFileTool(async () => fileSystemService);
|
|
175
275
|
const testFile = path.join(tempDir, "test.txt");
|
|
176
276
|
await fs.writeFile(testFile, "original");
|
|
177
277
|
const toolCallId = "test-call-fail";
|
|
@@ -179,15 +279,21 @@ describe("write_file tool", () => {
|
|
|
179
279
|
file_path: testFile,
|
|
180
280
|
content: "new content"
|
|
181
281
|
};
|
|
182
|
-
|
|
282
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
283
|
+
const previewFn = tool.presentation?.preview;
|
|
284
|
+
expect(previewFn).toBeDefined();
|
|
285
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
183
286
|
await fs.writeFile(testFile, "modified");
|
|
184
287
|
try {
|
|
185
|
-
await tool.execute(
|
|
288
|
+
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
186
289
|
} catch {
|
|
187
290
|
}
|
|
188
291
|
await fs.writeFile(testFile, "reset content");
|
|
189
|
-
await
|
|
190
|
-
const result = await tool.execute(
|
|
292
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
293
|
+
const result = await tool.execute(
|
|
294
|
+
parsedInput,
|
|
295
|
+
createToolContext(mockLogger, { toolCallId })
|
|
296
|
+
);
|
|
191
297
|
expect(result.success).toBe(true);
|
|
192
298
|
});
|
|
193
299
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dexto/tools-filesystem",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "FileSystem tools
|
|
3
|
+
"version": "1.6.1",
|
|
4
|
+
"description": "FileSystem tools factory for Dexto agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"glob": "^11.1.0",
|
|
23
23
|
"safe-regex": "^2.1.1",
|
|
24
24
|
"zod": "^3.25.0",
|
|
25
|
-
"@dexto/
|
|
25
|
+
"@dexto/agent-config": "1.6.1",
|
|
26
|
+
"@dexto/core": "1.6.1"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@types/diff": "^5.2.3",
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"README.md"
|
|
36
37
|
],
|
|
37
38
|
"scripts": {
|
|
38
|
-
"build": "tsup",
|
|
39
|
+
"build": "tsup && node ../../scripts/clean-tsbuildinfo.mjs && tsc -b tsconfig.json --emitDeclarationOnly",
|
|
39
40
|
"typecheck": "tsc --noEmit",
|
|
40
41
|
"clean": "rm -rf dist"
|
|
41
42
|
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { InternalTool } from '@dexto/core';
|
|
2
|
-
import { FileToolOptions } from './file-tool-types.cjs';
|
|
3
|
-
import './filesystem-service.cjs';
|
|
4
|
-
import './types.cjs';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Edit File Tool
|
|
8
|
-
*
|
|
9
|
-
* Internal tool for editing files by replacing text (requires approval)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Create the edit_file internal tool with directory approval support
|
|
14
|
-
*/
|
|
15
|
-
declare function createEditFileTool(options: FileToolOptions): InternalTool;
|
|
16
|
-
|
|
17
|
-
export { createEditFileTool };
|