@dexto/tools-filesystem 1.6.0 → 1.6.2
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 +44 -40
- package/dist/directory-approval.d.ts +8 -4
- package/dist/directory-approval.d.ts.map +1 -1
- package/dist/directory-approval.integration.test.cjs +107 -356
- package/dist/directory-approval.integration.test.d.ts +6 -6
- package/dist/directory-approval.integration.test.js +109 -360
- package/dist/directory-approval.js +45 -41
- package/dist/edit-file-tool.cjs +69 -47
- package/dist/edit-file-tool.d.ts.map +1 -1
- package/dist/edit-file-tool.js +77 -48
- package/dist/edit-file-tool.test.cjs +54 -11
- package/dist/edit-file-tool.test.js +54 -11
- package/dist/error-codes.cjs +4 -0
- package/dist/error-codes.d.ts +4 -0
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +4 -0
- package/dist/errors.cjs +48 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +48 -0
- package/dist/filesystem-service.cjs +307 -9
- package/dist/filesystem-service.d.ts +28 -1
- package/dist/filesystem-service.d.ts.map +1 -1
- package/dist/filesystem-service.js +308 -10
- package/dist/glob-files-tool.cjs +12 -1
- package/dist/glob-files-tool.d.ts.map +1 -1
- package/dist/glob-files-tool.js +13 -2
- package/dist/grep-content-tool.cjs +13 -1
- package/dist/grep-content-tool.d.ts.map +1 -1
- package/dist/grep-content-tool.js +14 -2
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +852 -16
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/path-validator.cjs +28 -2
- package/dist/path-validator.d.ts +14 -0
- package/dist/path-validator.d.ts.map +1 -1
- package/dist/path-validator.js +28 -2
- package/dist/read-file-tool.cjs +7 -1
- package/dist/read-file-tool.d.ts.map +1 -1
- package/dist/read-file-tool.js +8 -2
- package/dist/tool-factory.cjs +21 -0
- package/dist/tool-factory.d.ts.map +1 -1
- package/dist/tool-factory.js +21 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/write-file-tool.cjs +60 -38
- package/dist/write-file-tool.d.ts +1 -1
- package/dist/write-file-tool.d.ts.map +1 -1
- package/dist/write-file-tool.js +67 -39
- package/dist/write-file-tool.test.cjs +75 -13
- package/dist/write-file-tool.test.js +75 -13
- package/package.json +6 -6
- package/dist/directory-approval.d.cts +0 -22
- package/dist/directory-approval.integration.test.d.cts +0 -2
- package/dist/edit-file-tool.d.cts +0 -34
- 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 -18
- package/dist/filesystem-service.d.cts +0 -117
- package/dist/filesystem-service.test.d.cts +0 -2
- package/dist/glob-files-tool.d.cts +0 -31
- package/dist/grep-content-tool.d.cts +0 -40
- 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 -31
- package/dist/tool-factory-config.d.cts +0 -63
- package/dist/tool-factory.d.cts +0 -7
- package/dist/types.d.cts +0 -178
- package/dist/write-file-tool.d.cts +0 -34
- package/dist/write-file-tool.test.d.cts +0 -2
package/dist/write-file-tool.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { createPatch } from "diff";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createLocalToolCallHeader,
|
|
6
|
+
DextoRuntimeError,
|
|
7
|
+
ToolError,
|
|
8
|
+
defineTool,
|
|
9
|
+
truncateForHeader
|
|
10
|
+
} from "@dexto/core";
|
|
5
11
|
import { FileSystemErrorCode } from "./error-codes.js";
|
|
6
12
|
import { createDirectoryAccessApprovalHandlers, resolveFilePath } from "./directory-approval.js";
|
|
7
13
|
const previewContentHashCache = /* @__PURE__ */ new Map();
|
|
@@ -23,6 +29,7 @@ function generateDiffPreview(filePath, originalContent, newContent) {
|
|
|
23
29
|
const deletions = (unified.match(/^-[^-]/gm) || []).length;
|
|
24
30
|
return {
|
|
25
31
|
type: "diff",
|
|
32
|
+
title: "Update file",
|
|
26
33
|
unified,
|
|
27
34
|
filename: filePath,
|
|
28
35
|
additions,
|
|
@@ -32,55 +39,74 @@ function generateDiffPreview(filePath, originalContent, newContent) {
|
|
|
32
39
|
function createWriteFileTool(getFileSystemService) {
|
|
33
40
|
return defineTool({
|
|
34
41
|
id: "write_file",
|
|
35
|
-
displayName: "Write",
|
|
36
42
|
aliases: ["write"],
|
|
37
43
|
description: "Write content to a file. Creates a new file or overwrites existing file. Automatically creates backup of existing files before overwriting. Use create_dirs to create parent directories. Requires approval for all write operations. Returns success status, path, bytes written, and backup path if applicable.",
|
|
38
44
|
inputSchema: WriteFileInputSchema,
|
|
39
45
|
...createDirectoryAccessApprovalHandlers({
|
|
40
46
|
toolName: "write_file",
|
|
41
47
|
operation: "write",
|
|
48
|
+
inputSchema: WriteFileInputSchema,
|
|
42
49
|
getFileSystemService,
|
|
43
50
|
resolvePaths: (input, fileSystemService) => resolveFilePath(fileSystemService.getWorkingDirectory(), input.file_path)
|
|
44
51
|
}),
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
52
|
+
presentation: {
|
|
53
|
+
describeHeader: (input) => createLocalToolCallHeader({
|
|
54
|
+
title: "Write",
|
|
55
|
+
argsText: truncateForHeader(input.file_path, 140)
|
|
56
|
+
}),
|
|
57
|
+
/**
|
|
58
|
+
* Generate preview for approval UI - shows diff or file creation info
|
|
59
|
+
* Stores content hash for change detection in execute phase.
|
|
60
|
+
*/
|
|
61
|
+
preview: async (input, context) => {
|
|
62
|
+
const { file_path, content } = input;
|
|
63
|
+
const resolvedFileSystemService = await getFileSystemService(context);
|
|
64
|
+
const { path: resolvedPath } = resolveFilePath(
|
|
65
|
+
resolvedFileSystemService.getWorkingDirectory(),
|
|
66
|
+
file_path
|
|
67
|
+
);
|
|
68
|
+
try {
|
|
69
|
+
let originalContent;
|
|
70
|
+
try {
|
|
71
|
+
const originalFile = await resolvedFileSystemService.readFile(resolvedPath);
|
|
72
|
+
originalContent = originalFile.content;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.INVALID_PATH) {
|
|
75
|
+
const originalFile = await resolvedFileSystemService.readFileForToolPreview(
|
|
76
|
+
resolvedPath
|
|
77
|
+
);
|
|
78
|
+
originalContent = originalFile.content;
|
|
79
|
+
} else {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
68
83
|
if (context.toolCallId) {
|
|
69
|
-
previewContentHashCache.set(
|
|
84
|
+
previewContentHashCache.set(
|
|
85
|
+
context.toolCallId,
|
|
86
|
+
computeContentHash(originalContent)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return generateDiffPreview(resolvedPath, originalContent, content);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.FILE_NOT_FOUND) {
|
|
92
|
+
if (context.toolCallId) {
|
|
93
|
+
previewContentHashCache.set(context.toolCallId, FILE_NOT_EXISTS_MARKER);
|
|
94
|
+
}
|
|
95
|
+
const lineCount = content.split("\n").length;
|
|
96
|
+
const preview = {
|
|
97
|
+
type: "file",
|
|
98
|
+
title: "Create file",
|
|
99
|
+
path: resolvedPath,
|
|
100
|
+
operation: "create",
|
|
101
|
+
size: Buffer.byteLength(content, "utf8"),
|
|
102
|
+
lineCount,
|
|
103
|
+
content
|
|
104
|
+
// Include content for approval preview
|
|
105
|
+
};
|
|
106
|
+
return preview;
|
|
70
107
|
}
|
|
71
|
-
|
|
72
|
-
const preview = {
|
|
73
|
-
type: "file",
|
|
74
|
-
path: resolvedPath,
|
|
75
|
-
operation: "create",
|
|
76
|
-
size: Buffer.byteLength(content, "utf8"),
|
|
77
|
-
lineCount,
|
|
78
|
-
content
|
|
79
|
-
// Include content for approval preview
|
|
80
|
-
};
|
|
81
|
-
return preview;
|
|
108
|
+
throw error;
|
|
82
109
|
}
|
|
83
|
-
throw error;
|
|
84
110
|
}
|
|
85
111
|
},
|
|
86
112
|
async execute(input, context) {
|
|
@@ -136,10 +162,12 @@ function createWriteFileTool(getFileSystemService) {
|
|
|
136
162
|
const lineCount = content.split("\n").length;
|
|
137
163
|
_display = {
|
|
138
164
|
type: "file",
|
|
165
|
+
title: "Create file",
|
|
139
166
|
path: resolvedPath,
|
|
140
167
|
operation: "create",
|
|
141
168
|
size: result.bytesWritten,
|
|
142
|
-
lineCount
|
|
169
|
+
lineCount,
|
|
170
|
+
content
|
|
143
171
|
};
|
|
144
172
|
} else {
|
|
145
173
|
_display = generateDiffPreview(resolvedPath, originalContent, content);
|
|
@@ -38,6 +38,7 @@ const createMockLogger = () => {
|
|
|
38
38
|
error: import_vitest.vi.fn(),
|
|
39
39
|
trackException: import_vitest.vi.fn(),
|
|
40
40
|
createChild: import_vitest.vi.fn(() => logger),
|
|
41
|
+
createFileOnlyChild: import_vitest.vi.fn(() => logger),
|
|
41
42
|
setLevel: import_vitest.vi.fn(),
|
|
42
43
|
getLevel: import_vitest.vi.fn(() => "debug"),
|
|
43
44
|
getLogFilePath: import_vitest.vi.fn(() => null),
|
|
@@ -78,6 +79,36 @@ function createToolContext(logger, overrides = {}) {
|
|
|
78
79
|
}
|
|
79
80
|
});
|
|
80
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
|
+
});
|
|
81
112
|
(0, import_vitest.it)("should succeed when existing file is not modified between preview and execute", async () => {
|
|
82
113
|
const tool = (0, import_write_file_tool.createWriteFileTool)(async () => fileSystemService);
|
|
83
114
|
const testFile = path.join(tempDir, "test.txt");
|
|
@@ -88,12 +119,17 @@ function createToolContext(logger, overrides = {}) {
|
|
|
88
119
|
content: "new content"
|
|
89
120
|
};
|
|
90
121
|
const parsedInput = tool.inputSchema.parse(input);
|
|
91
|
-
const preview = await tool.
|
|
122
|
+
const preview = await tool.presentation.preview(
|
|
92
123
|
parsedInput,
|
|
93
124
|
createToolContext(mockLogger, { toolCallId })
|
|
94
125
|
);
|
|
95
126
|
(0, import_vitest.expect)(preview).toBeDefined();
|
|
96
127
|
(0, import_vitest.expect)(preview?.type).toBe("diff");
|
|
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
|
+
}
|
|
97
133
|
const result = await tool.execute(
|
|
98
134
|
parsedInput,
|
|
99
135
|
createToolContext(mockLogger, { toolCallId })
|
|
@@ -113,7 +149,10 @@ function createToolContext(logger, overrides = {}) {
|
|
|
113
149
|
content: "new content"
|
|
114
150
|
};
|
|
115
151
|
const parsedInput = tool.inputSchema.parse(input);
|
|
116
|
-
await tool.
|
|
152
|
+
await tool.presentation.preview(
|
|
153
|
+
parsedInput,
|
|
154
|
+
createToolContext(mockLogger, { toolCallId })
|
|
155
|
+
);
|
|
117
156
|
await fs.writeFile(testFile, "user modified this");
|
|
118
157
|
try {
|
|
119
158
|
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
@@ -137,7 +176,10 @@ function createToolContext(logger, overrides = {}) {
|
|
|
137
176
|
content: "new content"
|
|
138
177
|
};
|
|
139
178
|
const parsedInput = tool.inputSchema.parse(input);
|
|
140
|
-
await tool.
|
|
179
|
+
await tool.presentation.preview(
|
|
180
|
+
parsedInput,
|
|
181
|
+
createToolContext(mockLogger, { toolCallId })
|
|
182
|
+
);
|
|
141
183
|
await fs.unlink(testFile);
|
|
142
184
|
try {
|
|
143
185
|
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
@@ -160,23 +202,39 @@ function createToolContext(logger, overrides = {}) {
|
|
|
160
202
|
content: "brand new content"
|
|
161
203
|
};
|
|
162
204
|
const parsedInput = tool.inputSchema.parse(input);
|
|
163
|
-
const preview = await tool.
|
|
205
|
+
const preview = await tool.presentation.preview(
|
|
164
206
|
parsedInput,
|
|
165
207
|
createToolContext(mockLogger, { toolCallId })
|
|
166
208
|
);
|
|
167
209
|
(0, import_vitest.expect)(preview).toBeDefined();
|
|
168
210
|
(0, import_vitest.expect)(preview?.type).toBe("file");
|
|
169
|
-
|
|
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
|
+
}
|
|
170
217
|
const result = await tool.execute(
|
|
171
218
|
parsedInput,
|
|
172
219
|
createToolContext(mockLogger, { toolCallId })
|
|
173
220
|
);
|
|
174
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
|
+
}
|
|
175
231
|
const content = await fs.readFile(testFile, "utf-8");
|
|
176
232
|
(0, import_vitest.expect)(content).toBe("brand new content");
|
|
177
233
|
});
|
|
178
234
|
(0, import_vitest.it)("should fail when file is created by someone else between preview and execute", async () => {
|
|
179
235
|
const tool = (0, import_write_file_tool.createWriteFileTool)(async () => fileSystemService);
|
|
236
|
+
const previewFn = tool.presentation?.preview;
|
|
237
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
180
238
|
const testFile = path.join(tempDir, "race-condition.txt");
|
|
181
239
|
const toolCallId = "test-call-race";
|
|
182
240
|
const input = {
|
|
@@ -184,7 +242,7 @@ function createToolContext(logger, overrides = {}) {
|
|
|
184
242
|
content: "agent content"
|
|
185
243
|
};
|
|
186
244
|
const parsedInput = tool.inputSchema.parse(input);
|
|
187
|
-
const preview = await
|
|
245
|
+
const preview = await previewFn(
|
|
188
246
|
parsedInput,
|
|
189
247
|
createToolContext(mockLogger, { toolCallId })
|
|
190
248
|
);
|
|
@@ -214,17 +272,19 @@ function createToolContext(logger, overrides = {}) {
|
|
|
214
272
|
content: "first write"
|
|
215
273
|
};
|
|
216
274
|
const parsedInput = tool.inputSchema.parse(input);
|
|
217
|
-
await tool.
|
|
275
|
+
await tool.presentation.preview(
|
|
276
|
+
parsedInput,
|
|
277
|
+
createToolContext(mockLogger, { toolCallId })
|
|
278
|
+
);
|
|
218
279
|
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
219
280
|
const input2 = {
|
|
220
281
|
file_path: testFile,
|
|
221
282
|
content: "second write"
|
|
222
283
|
};
|
|
223
284
|
const parsedInput2 = tool.inputSchema.parse(input2);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
);
|
|
285
|
+
const previewFn2 = tool.presentation?.preview;
|
|
286
|
+
(0, import_vitest.expect)(previewFn2).toBeDefined();
|
|
287
|
+
await previewFn2(parsedInput2, createToolContext(mockLogger, { toolCallId }));
|
|
228
288
|
const result = await tool.execute(
|
|
229
289
|
parsedInput2,
|
|
230
290
|
createToolContext(mockLogger, { toolCallId })
|
|
@@ -243,14 +303,16 @@ function createToolContext(logger, overrides = {}) {
|
|
|
243
303
|
content: "new content"
|
|
244
304
|
};
|
|
245
305
|
const parsedInput = tool.inputSchema.parse(input);
|
|
246
|
-
|
|
306
|
+
const previewFn = tool.presentation?.preview;
|
|
307
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
308
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
247
309
|
await fs.writeFile(testFile, "modified");
|
|
248
310
|
try {
|
|
249
311
|
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
250
312
|
} catch {
|
|
251
313
|
}
|
|
252
314
|
await fs.writeFile(testFile, "reset content");
|
|
253
|
-
await
|
|
315
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
254
316
|
const result = await tool.execute(
|
|
255
317
|
parsedInput,
|
|
256
318
|
createToolContext(mockLogger, { toolCallId })
|
|
@@ -15,6 +15,7 @@ const createMockLogger = () => {
|
|
|
15
15
|
error: vi.fn(),
|
|
16
16
|
trackException: vi.fn(),
|
|
17
17
|
createChild: vi.fn(() => logger),
|
|
18
|
+
createFileOnlyChild: vi.fn(() => logger),
|
|
18
19
|
setLevel: vi.fn(),
|
|
19
20
|
getLevel: vi.fn(() => "debug"),
|
|
20
21
|
getLogFilePath: vi.fn(() => null),
|
|
@@ -55,6 +56,36 @@ describe("write_file tool", () => {
|
|
|
55
56
|
}
|
|
56
57
|
});
|
|
57
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
|
+
});
|
|
58
89
|
it("should succeed when existing file is not modified between preview and execute", async () => {
|
|
59
90
|
const tool = createWriteFileTool(async () => fileSystemService);
|
|
60
91
|
const testFile = path.join(tempDir, "test.txt");
|
|
@@ -65,12 +96,17 @@ describe("write_file tool", () => {
|
|
|
65
96
|
content: "new content"
|
|
66
97
|
};
|
|
67
98
|
const parsedInput = tool.inputSchema.parse(input);
|
|
68
|
-
const preview = await tool.
|
|
99
|
+
const preview = await tool.presentation.preview(
|
|
69
100
|
parsedInput,
|
|
70
101
|
createToolContext(mockLogger, { toolCallId })
|
|
71
102
|
);
|
|
72
103
|
expect(preview).toBeDefined();
|
|
73
104
|
expect(preview?.type).toBe("diff");
|
|
105
|
+
if (preview?.type === "diff") {
|
|
106
|
+
expect(preview.title).toBe("Update file");
|
|
107
|
+
} else {
|
|
108
|
+
expect.fail("Expected diff preview");
|
|
109
|
+
}
|
|
74
110
|
const result = await tool.execute(
|
|
75
111
|
parsedInput,
|
|
76
112
|
createToolContext(mockLogger, { toolCallId })
|
|
@@ -90,7 +126,10 @@ describe("write_file tool", () => {
|
|
|
90
126
|
content: "new content"
|
|
91
127
|
};
|
|
92
128
|
const parsedInput = tool.inputSchema.parse(input);
|
|
93
|
-
await tool.
|
|
129
|
+
await tool.presentation.preview(
|
|
130
|
+
parsedInput,
|
|
131
|
+
createToolContext(mockLogger, { toolCallId })
|
|
132
|
+
);
|
|
94
133
|
await fs.writeFile(testFile, "user modified this");
|
|
95
134
|
try {
|
|
96
135
|
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
@@ -114,7 +153,10 @@ describe("write_file tool", () => {
|
|
|
114
153
|
content: "new content"
|
|
115
154
|
};
|
|
116
155
|
const parsedInput = tool.inputSchema.parse(input);
|
|
117
|
-
await tool.
|
|
156
|
+
await tool.presentation.preview(
|
|
157
|
+
parsedInput,
|
|
158
|
+
createToolContext(mockLogger, { toolCallId })
|
|
159
|
+
);
|
|
118
160
|
await fs.unlink(testFile);
|
|
119
161
|
try {
|
|
120
162
|
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
@@ -137,23 +179,39 @@ describe("write_file tool", () => {
|
|
|
137
179
|
content: "brand new content"
|
|
138
180
|
};
|
|
139
181
|
const parsedInput = tool.inputSchema.parse(input);
|
|
140
|
-
const preview = await tool.
|
|
182
|
+
const preview = await tool.presentation.preview(
|
|
141
183
|
parsedInput,
|
|
142
184
|
createToolContext(mockLogger, { toolCallId })
|
|
143
185
|
);
|
|
144
186
|
expect(preview).toBeDefined();
|
|
145
187
|
expect(preview?.type).toBe("file");
|
|
146
|
-
|
|
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
|
+
}
|
|
147
194
|
const result = await tool.execute(
|
|
148
195
|
parsedInput,
|
|
149
196
|
createToolContext(mockLogger, { toolCallId })
|
|
150
197
|
);
|
|
151
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
|
+
}
|
|
152
208
|
const content = await fs.readFile(testFile, "utf-8");
|
|
153
209
|
expect(content).toBe("brand new content");
|
|
154
210
|
});
|
|
155
211
|
it("should fail when file is created by someone else between preview and execute", async () => {
|
|
156
212
|
const tool = createWriteFileTool(async () => fileSystemService);
|
|
213
|
+
const previewFn = tool.presentation?.preview;
|
|
214
|
+
expect(previewFn).toBeDefined();
|
|
157
215
|
const testFile = path.join(tempDir, "race-condition.txt");
|
|
158
216
|
const toolCallId = "test-call-race";
|
|
159
217
|
const input = {
|
|
@@ -161,7 +219,7 @@ describe("write_file tool", () => {
|
|
|
161
219
|
content: "agent content"
|
|
162
220
|
};
|
|
163
221
|
const parsedInput = tool.inputSchema.parse(input);
|
|
164
|
-
const preview = await
|
|
222
|
+
const preview = await previewFn(
|
|
165
223
|
parsedInput,
|
|
166
224
|
createToolContext(mockLogger, { toolCallId })
|
|
167
225
|
);
|
|
@@ -191,17 +249,19 @@ describe("write_file tool", () => {
|
|
|
191
249
|
content: "first write"
|
|
192
250
|
};
|
|
193
251
|
const parsedInput = tool.inputSchema.parse(input);
|
|
194
|
-
await tool.
|
|
252
|
+
await tool.presentation.preview(
|
|
253
|
+
parsedInput,
|
|
254
|
+
createToolContext(mockLogger, { toolCallId })
|
|
255
|
+
);
|
|
195
256
|
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
196
257
|
const input2 = {
|
|
197
258
|
file_path: testFile,
|
|
198
259
|
content: "second write"
|
|
199
260
|
};
|
|
200
261
|
const parsedInput2 = tool.inputSchema.parse(input2);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
);
|
|
262
|
+
const previewFn2 = tool.presentation?.preview;
|
|
263
|
+
expect(previewFn2).toBeDefined();
|
|
264
|
+
await previewFn2(parsedInput2, createToolContext(mockLogger, { toolCallId }));
|
|
205
265
|
const result = await tool.execute(
|
|
206
266
|
parsedInput2,
|
|
207
267
|
createToolContext(mockLogger, { toolCallId })
|
|
@@ -220,14 +280,16 @@ describe("write_file tool", () => {
|
|
|
220
280
|
content: "new content"
|
|
221
281
|
};
|
|
222
282
|
const parsedInput = tool.inputSchema.parse(input);
|
|
223
|
-
|
|
283
|
+
const previewFn = tool.presentation?.preview;
|
|
284
|
+
expect(previewFn).toBeDefined();
|
|
285
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
224
286
|
await fs.writeFile(testFile, "modified");
|
|
225
287
|
try {
|
|
226
288
|
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
227
289
|
} catch {
|
|
228
290
|
}
|
|
229
291
|
await fs.writeFile(testFile, "reset content");
|
|
230
|
-
await
|
|
292
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
231
293
|
const result = await tool.execute(
|
|
232
294
|
parsedInput,
|
|
233
295
|
createToolContext(mockLogger, { toolCallId })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dexto/tools-filesystem",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "FileSystem tools factory for Dexto agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"file-operations"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"diff": "^
|
|
22
|
-
"glob": "^
|
|
21
|
+
"diff": "^8.0.3",
|
|
22
|
+
"glob": "^12.0.0",
|
|
23
23
|
"safe-regex": "^2.1.1",
|
|
24
24
|
"zod": "^3.25.0",
|
|
25
|
-
"@dexto/agent-config": "1.6.
|
|
26
|
-
"@dexto/core": "1.6.
|
|
25
|
+
"@dexto/agent-config": "1.6.2",
|
|
26
|
+
"@dexto/core": "1.6.2"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/diff": "^5.2.3",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"README.md"
|
|
37
37
|
],
|
|
38
38
|
"scripts": {
|
|
39
|
-
"build": "tsup",
|
|
39
|
+
"build": "tsup && node ../../scripts/clean-tsbuildinfo.mjs && tsc -b tsconfig.json --emitDeclarationOnly",
|
|
40
40
|
"typecheck": "tsc --noEmit",
|
|
41
41
|
"clean": "rm -rf dist"
|
|
42
42
|
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { ToolExecutionContext, ApprovalRequestDetails, ApprovalResponse } from '@dexto/core';
|
|
2
|
-
import { FileSystemService } from './filesystem-service.cjs';
|
|
3
|
-
import { FileSystemServiceGetter } from './file-tool-types.cjs';
|
|
4
|
-
import './types.cjs';
|
|
5
|
-
|
|
6
|
-
type DirectoryApprovalOperation = 'read' | 'write' | 'edit';
|
|
7
|
-
type DirectoryApprovalPaths = {
|
|
8
|
-
path: string;
|
|
9
|
-
parentDir: string;
|
|
10
|
-
};
|
|
11
|
-
declare function resolveFilePath(workingDirectory: string, filePath: string): DirectoryApprovalPaths;
|
|
12
|
-
declare function createDirectoryAccessApprovalHandlers<TInput>(options: {
|
|
13
|
-
toolName: string;
|
|
14
|
-
operation: DirectoryApprovalOperation;
|
|
15
|
-
getFileSystemService: FileSystemServiceGetter;
|
|
16
|
-
resolvePaths: (input: TInput, fileSystemService: FileSystemService) => DirectoryApprovalPaths;
|
|
17
|
-
}): {
|
|
18
|
-
getApprovalOverride: (input: TInput, context: ToolExecutionContext) => Promise<ApprovalRequestDetails | null>;
|
|
19
|
-
onApprovalGranted: (response: ApprovalResponse, context: ToolExecutionContext, approvalRequest: ApprovalRequestDetails) => void;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export { createDirectoryAccessApprovalHandlers, resolveFilePath };
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { Tool } from '@dexto/core';
|
|
3
|
-
import { FileSystemServiceGetter } from './file-tool-types.cjs';
|
|
4
|
-
import './filesystem-service.cjs';
|
|
5
|
-
import './types.cjs';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Edit File Tool
|
|
9
|
-
*
|
|
10
|
-
* Internal tool for editing files by replacing text (requires approval)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
declare const EditFileInputSchema: z.ZodObject<{
|
|
14
|
-
file_path: z.ZodString;
|
|
15
|
-
old_string: z.ZodString;
|
|
16
|
-
new_string: z.ZodString;
|
|
17
|
-
replace_all: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
18
|
-
}, "strict", z.ZodTypeAny, {
|
|
19
|
-
file_path: string;
|
|
20
|
-
old_string: string;
|
|
21
|
-
new_string: string;
|
|
22
|
-
replace_all: boolean;
|
|
23
|
-
}, {
|
|
24
|
-
file_path: string;
|
|
25
|
-
old_string: string;
|
|
26
|
-
new_string: string;
|
|
27
|
-
replace_all?: boolean | undefined;
|
|
28
|
-
}>;
|
|
29
|
-
/**
|
|
30
|
-
* Create the edit_file internal tool with directory approval support
|
|
31
|
-
*/
|
|
32
|
-
declare function createEditFileTool(getFileSystemService: FileSystemServiceGetter): Tool<typeof EditFileInputSchema>;
|
|
33
|
-
|
|
34
|
-
export { createEditFileTool };
|
package/dist/error-codes.d.cts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FileSystem Service Error Codes
|
|
3
|
-
*
|
|
4
|
-
* Standardized error codes for file system operations
|
|
5
|
-
*/
|
|
6
|
-
declare enum FileSystemErrorCode {
|
|
7
|
-
FILE_NOT_FOUND = "FILESYSTEM_FILE_NOT_FOUND",
|
|
8
|
-
DIRECTORY_NOT_FOUND = "FILESYSTEM_DIRECTORY_NOT_FOUND",
|
|
9
|
-
PERMISSION_DENIED = "FILESYSTEM_PERMISSION_DENIED",
|
|
10
|
-
PATH_NOT_ALLOWED = "FILESYSTEM_PATH_NOT_ALLOWED",
|
|
11
|
-
PATH_BLOCKED = "FILESYSTEM_PATH_BLOCKED",
|
|
12
|
-
INVALID_PATH = "FILESYSTEM_INVALID_PATH",
|
|
13
|
-
PATH_TRAVERSAL_DETECTED = "FILESYSTEM_PATH_TRAVERSAL_DETECTED",
|
|
14
|
-
INVALID_FILE_EXTENSION = "FILESYSTEM_INVALID_FILE_EXTENSION",
|
|
15
|
-
INVALID_ENCODING = "FILESYSTEM_INVALID_ENCODING",
|
|
16
|
-
FILE_TOO_LARGE = "FILESYSTEM_FILE_TOO_LARGE",
|
|
17
|
-
TOO_MANY_RESULTS = "FILESYSTEM_TOO_MANY_RESULTS",
|
|
18
|
-
READ_FAILED = "FILESYSTEM_READ_FAILED",
|
|
19
|
-
WRITE_FAILED = "FILESYSTEM_WRITE_FAILED",
|
|
20
|
-
BACKUP_FAILED = "FILESYSTEM_BACKUP_FAILED",
|
|
21
|
-
EDIT_FAILED = "FILESYSTEM_EDIT_FAILED",
|
|
22
|
-
STRING_NOT_UNIQUE = "FILESYSTEM_STRING_NOT_UNIQUE",
|
|
23
|
-
STRING_NOT_FOUND = "FILESYSTEM_STRING_NOT_FOUND",
|
|
24
|
-
GLOB_FAILED = "FILESYSTEM_GLOB_FAILED",
|
|
25
|
-
SEARCH_FAILED = "FILESYSTEM_SEARCH_FAILED",
|
|
26
|
-
INVALID_PATTERN = "FILESYSTEM_INVALID_PATTERN",
|
|
27
|
-
REGEX_TIMEOUT = "FILESYSTEM_REGEX_TIMEOUT",
|
|
28
|
-
INVALID_CONFIG = "FILESYSTEM_INVALID_CONFIG",
|
|
29
|
-
SERVICE_NOT_INITIALIZED = "FILESYSTEM_SERVICE_NOT_INITIALIZED"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export { FileSystemErrorCode };
|