@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/edit-file-tool.cjs
CHANGED
|
@@ -45,6 +45,7 @@ function generateDiffPreview(filePath, originalContent, newContent) {
|
|
|
45
45
|
const deletions = (unified.match(/^-[^-]/gm) || []).length;
|
|
46
46
|
return {
|
|
47
47
|
type: "diff",
|
|
48
|
+
title: "Update file",
|
|
48
49
|
unified,
|
|
49
50
|
filename: filePath,
|
|
50
51
|
additions,
|
|
@@ -54,67 +55,88 @@ function generateDiffPreview(filePath, originalContent, newContent) {
|
|
|
54
55
|
function createEditFileTool(getFileSystemService) {
|
|
55
56
|
return (0, import_core.defineTool)({
|
|
56
57
|
id: "edit_file",
|
|
57
|
-
displayName: "Update",
|
|
58
58
|
aliases: ["edit"],
|
|
59
59
|
description: "Edit a file by replacing text. By default, old_string must be unique in the file (will error if found multiple times). Set replace_all=true to replace all occurrences. Automatically creates backup before editing. Requires approval. Returns success status, path, number of changes made, and backup path.",
|
|
60
60
|
inputSchema: EditFileInputSchema,
|
|
61
61
|
...(0, import_directory_approval.createDirectoryAccessApprovalHandlers)({
|
|
62
62
|
toolName: "edit_file",
|
|
63
63
|
operation: "edit",
|
|
64
|
+
inputSchema: EditFileInputSchema,
|
|
64
65
|
getFileSystemService,
|
|
65
66
|
resolvePaths: (input, fileSystemService) => (0, import_directory_approval.resolveFilePath)(fileSystemService.getWorkingDirectory(), input.file_path)
|
|
66
67
|
}),
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
68
|
+
presentation: {
|
|
69
|
+
describeHeader: (input) => (0, import_core.createLocalToolCallHeader)({
|
|
70
|
+
title: "Update",
|
|
71
|
+
argsText: (0, import_core.truncateForHeader)(input.file_path, 140)
|
|
72
|
+
}),
|
|
73
|
+
/**
|
|
74
|
+
* Generate preview for approval UI - shows diff without modifying file
|
|
75
|
+
* Throws ToolError.validationFailed() for validation errors (file not found, string not found)
|
|
76
|
+
* Stores content hash for change detection in execute phase.
|
|
77
|
+
*/
|
|
78
|
+
preview: async (input, context) => {
|
|
79
|
+
const { file_path, old_string, new_string, replace_all } = input;
|
|
80
|
+
const resolvedFileSystemService = await getFileSystemService(context);
|
|
81
|
+
const { path: resolvedPath } = (0, import_directory_approval.resolveFilePath)(
|
|
82
|
+
resolvedFileSystemService.getWorkingDirectory(),
|
|
83
|
+
file_path
|
|
84
|
+
);
|
|
85
|
+
try {
|
|
86
|
+
let originalContent;
|
|
87
|
+
try {
|
|
88
|
+
const originalFile = await resolvedFileSystemService.readFile(resolvedPath);
|
|
89
|
+
originalContent = originalFile.content;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof import_core.DextoRuntimeError && error.code === import_error_codes.FileSystemErrorCode.INVALID_PATH) {
|
|
92
|
+
const originalFile = await resolvedFileSystemService.readFileForToolPreview(
|
|
93
|
+
resolvedPath
|
|
94
|
+
);
|
|
95
|
+
originalContent = originalFile.content;
|
|
96
|
+
} else {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (context.toolCallId) {
|
|
101
|
+
previewContentHashCache.set(
|
|
102
|
+
context.toolCallId,
|
|
103
|
+
computeContentHash(originalContent)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (!replace_all) {
|
|
107
|
+
const occurrences = originalContent.split(old_string).length - 1;
|
|
108
|
+
if (occurrences > 1) {
|
|
109
|
+
throw import_core.ToolError.validationFailed(
|
|
110
|
+
"edit_file",
|
|
111
|
+
`String found ${occurrences} times in file. Set replace_all=true to replace all, or provide more context to make old_string unique.`,
|
|
112
|
+
{ file_path: resolvedPath, occurrences }
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const newContent = replace_all ? originalContent.split(old_string).join(new_string) : originalContent.replace(old_string, new_string);
|
|
117
|
+
if (originalContent === newContent) {
|
|
91
118
|
throw import_core.ToolError.validationFailed(
|
|
92
119
|
"edit_file",
|
|
93
|
-
`String found
|
|
94
|
-
{
|
|
120
|
+
`String not found in file: "${old_string.slice(0, 50)}${old_string.length > 50 ? "..." : ""}"`,
|
|
121
|
+
{
|
|
122
|
+
file_path: resolvedPath,
|
|
123
|
+
old_string_preview: old_string.slice(0, 100)
|
|
124
|
+
}
|
|
95
125
|
);
|
|
96
126
|
}
|
|
127
|
+
return generateDiffPreview(resolvedPath, originalContent, newContent);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error instanceof import_core.DextoRuntimeError && error.code === import_core.ToolErrorCode.VALIDATION_FAILED) {
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
if (error instanceof import_core.DextoRuntimeError) {
|
|
133
|
+
throw import_core.ToolError.validationFailed("edit_file", error.message, {
|
|
134
|
+
file_path: resolvedPath,
|
|
135
|
+
originalErrorCode: error.code
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
97
139
|
}
|
|
98
|
-
const newContent = replace_all ? originalContent.split(old_string).join(new_string) : originalContent.replace(old_string, new_string);
|
|
99
|
-
if (originalContent === newContent) {
|
|
100
|
-
throw import_core.ToolError.validationFailed(
|
|
101
|
-
"edit_file",
|
|
102
|
-
`String not found in file: "${old_string.slice(0, 50)}${old_string.length > 50 ? "..." : ""}"`,
|
|
103
|
-
{ file_path: resolvedPath, old_string_preview: old_string.slice(0, 100) }
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
return generateDiffPreview(resolvedPath, originalContent, newContent);
|
|
107
|
-
} catch (error) {
|
|
108
|
-
if (error instanceof import_core.DextoRuntimeError && error.code === import_core.ToolErrorCode.VALIDATION_FAILED) {
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
if (error instanceof import_core.DextoRuntimeError) {
|
|
112
|
-
throw import_core.ToolError.validationFailed("edit_file", error.message, {
|
|
113
|
-
file_path: resolvedPath,
|
|
114
|
-
originalErrorCode: error.code
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
return null;
|
|
118
140
|
}
|
|
119
141
|
},
|
|
120
142
|
async execute(input, context) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-file-tool.d.ts","sourceRoot":"","sources":["../src/edit-file-tool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"edit-file-tool.d.ts","sourceRoot":"","sources":["../src/edit-file-tool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAUxB,OAAO,KAAK,EAAE,IAAI,EAAwB,MAAM,aAAa,CAAC;AAE9D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAkBpE,QAAA,MAAM,mBAAmB;;;;;;;;;;;;;;;EAaZ,CAAC;AA0Bd;;GAEG;AACH,wBAAgB,kBAAkB,CAC9B,oBAAoB,EAAE,uBAAuB,GAC9C,IAAI,CAAC,OAAO,mBAAmB,CAAC,CA2LlC"}
|
package/dist/edit-file-tool.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
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
|
+
ToolErrorCode,
|
|
9
|
+
defineTool,
|
|
10
|
+
truncateForHeader
|
|
11
|
+
} from "@dexto/core";
|
|
5
12
|
import { FileSystemErrorCode } from "./error-codes.js";
|
|
6
13
|
import { createDirectoryAccessApprovalHandlers, resolveFilePath } from "./directory-approval.js";
|
|
7
14
|
const previewContentHashCache = /* @__PURE__ */ new Map();
|
|
@@ -22,6 +29,7 @@ function generateDiffPreview(filePath, originalContent, newContent) {
|
|
|
22
29
|
const deletions = (unified.match(/^-[^-]/gm) || []).length;
|
|
23
30
|
return {
|
|
24
31
|
type: "diff",
|
|
32
|
+
title: "Update file",
|
|
25
33
|
unified,
|
|
26
34
|
filename: filePath,
|
|
27
35
|
additions,
|
|
@@ -31,67 +39,88 @@ function generateDiffPreview(filePath, originalContent, newContent) {
|
|
|
31
39
|
function createEditFileTool(getFileSystemService) {
|
|
32
40
|
return defineTool({
|
|
33
41
|
id: "edit_file",
|
|
34
|
-
displayName: "Update",
|
|
35
42
|
aliases: ["edit"],
|
|
36
43
|
description: "Edit a file by replacing text. By default, old_string must be unique in the file (will error if found multiple times). Set replace_all=true to replace all occurrences. Automatically creates backup before editing. Requires approval. Returns success status, path, number of changes made, and backup path.",
|
|
37
44
|
inputSchema: EditFileInputSchema,
|
|
38
45
|
...createDirectoryAccessApprovalHandlers({
|
|
39
46
|
toolName: "edit_file",
|
|
40
47
|
operation: "edit",
|
|
48
|
+
inputSchema: EditFileInputSchema,
|
|
41
49
|
getFileSystemService,
|
|
42
50
|
resolvePaths: (input, fileSystemService) => resolveFilePath(fileSystemService.getWorkingDirectory(), input.file_path)
|
|
43
51
|
}),
|
|
44
|
-
|
|
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: "Update",
|
|
55
|
+
argsText: truncateForHeader(input.file_path, 140)
|
|
56
|
+
}),
|
|
57
|
+
/**
|
|
58
|
+
* Generate preview for approval UI - shows diff without modifying file
|
|
59
|
+
* Throws ToolError.validationFailed() for validation errors (file not found, string not found)
|
|
60
|
+
* Stores content hash for change detection in execute phase.
|
|
61
|
+
*/
|
|
62
|
+
preview: async (input, context) => {
|
|
63
|
+
const { file_path, old_string, new_string, replace_all } = input;
|
|
64
|
+
const resolvedFileSystemService = await getFileSystemService(context);
|
|
65
|
+
const { path: resolvedPath } = resolveFilePath(
|
|
66
|
+
resolvedFileSystemService.getWorkingDirectory(),
|
|
67
|
+
file_path
|
|
68
|
+
);
|
|
69
|
+
try {
|
|
70
|
+
let originalContent;
|
|
71
|
+
try {
|
|
72
|
+
const originalFile = await resolvedFileSystemService.readFile(resolvedPath);
|
|
73
|
+
originalContent = originalFile.content;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.INVALID_PATH) {
|
|
76
|
+
const originalFile = await resolvedFileSystemService.readFileForToolPreview(
|
|
77
|
+
resolvedPath
|
|
78
|
+
);
|
|
79
|
+
originalContent = originalFile.content;
|
|
80
|
+
} else {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (context.toolCallId) {
|
|
85
|
+
previewContentHashCache.set(
|
|
86
|
+
context.toolCallId,
|
|
87
|
+
computeContentHash(originalContent)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (!replace_all) {
|
|
91
|
+
const occurrences = originalContent.split(old_string).length - 1;
|
|
92
|
+
if (occurrences > 1) {
|
|
93
|
+
throw ToolError.validationFailed(
|
|
94
|
+
"edit_file",
|
|
95
|
+
`String found ${occurrences} times in file. Set replace_all=true to replace all, or provide more context to make old_string unique.`,
|
|
96
|
+
{ file_path: resolvedPath, occurrences }
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const newContent = replace_all ? originalContent.split(old_string).join(new_string) : originalContent.replace(old_string, new_string);
|
|
101
|
+
if (originalContent === newContent) {
|
|
68
102
|
throw ToolError.validationFailed(
|
|
69
103
|
"edit_file",
|
|
70
|
-
`String found
|
|
71
|
-
{
|
|
104
|
+
`String not found in file: "${old_string.slice(0, 50)}${old_string.length > 50 ? "..." : ""}"`,
|
|
105
|
+
{
|
|
106
|
+
file_path: resolvedPath,
|
|
107
|
+
old_string_preview: old_string.slice(0, 100)
|
|
108
|
+
}
|
|
72
109
|
);
|
|
73
110
|
}
|
|
111
|
+
return generateDiffPreview(resolvedPath, originalContent, newContent);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error instanceof DextoRuntimeError && error.code === ToolErrorCode.VALIDATION_FAILED) {
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
if (error instanceof DextoRuntimeError) {
|
|
117
|
+
throw ToolError.validationFailed("edit_file", error.message, {
|
|
118
|
+
file_path: resolvedPath,
|
|
119
|
+
originalErrorCode: error.code
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
74
123
|
}
|
|
75
|
-
const newContent = replace_all ? originalContent.split(old_string).join(new_string) : originalContent.replace(old_string, new_string);
|
|
76
|
-
if (originalContent === newContent) {
|
|
77
|
-
throw ToolError.validationFailed(
|
|
78
|
-
"edit_file",
|
|
79
|
-
`String not found in file: "${old_string.slice(0, 50)}${old_string.length > 50 ? "..." : ""}"`,
|
|
80
|
-
{ file_path: resolvedPath, old_string_preview: old_string.slice(0, 100) }
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
return generateDiffPreview(resolvedPath, originalContent, newContent);
|
|
84
|
-
} catch (error) {
|
|
85
|
-
if (error instanceof DextoRuntimeError && error.code === ToolErrorCode.VALIDATION_FAILED) {
|
|
86
|
-
throw error;
|
|
87
|
-
}
|
|
88
|
-
if (error instanceof DextoRuntimeError) {
|
|
89
|
-
throw ToolError.validationFailed("edit_file", error.message, {
|
|
90
|
-
file_path: resolvedPath,
|
|
91
|
-
originalErrorCode: error.code
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
return null;
|
|
95
124
|
}
|
|
96
125
|
},
|
|
97
126
|
async execute(input, context) {
|
|
@@ -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,8 +79,43 @@ function createToolContext(logger, overrides = {}) {
|
|
|
78
79
|
}
|
|
79
80
|
});
|
|
80
81
|
(0, import_vitest.describe)("File Modification Detection", () => {
|
|
82
|
+
(0, import_vitest.it)("should generate preview for files outside config-allowed roots (preview read only)", async () => {
|
|
83
|
+
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
84
|
+
const previewFn = tool.presentation?.preview;
|
|
85
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
86
|
+
const rawExternalDir = await fs.mkdtemp(
|
|
87
|
+
path.join(os.tmpdir(), "dexto-edit-outside-allowed-")
|
|
88
|
+
);
|
|
89
|
+
const externalDir = await fs.realpath(rawExternalDir);
|
|
90
|
+
const externalFile = path.join(externalDir, "external.txt");
|
|
91
|
+
try {
|
|
92
|
+
await fs.writeFile(externalFile, "hello world");
|
|
93
|
+
const toolCallId = "preview-outside-roots";
|
|
94
|
+
const parsedInput = tool.inputSchema.parse({
|
|
95
|
+
file_path: externalFile,
|
|
96
|
+
old_string: "world",
|
|
97
|
+
new_string: "universe"
|
|
98
|
+
});
|
|
99
|
+
const preview = await previewFn(
|
|
100
|
+
parsedInput,
|
|
101
|
+
createToolContext(mockLogger, { toolCallId })
|
|
102
|
+
);
|
|
103
|
+
(0, import_vitest.expect)(preview).toBeDefined();
|
|
104
|
+
(0, import_vitest.expect)(preview?.type).toBe("diff");
|
|
105
|
+
if (preview?.type === "diff") {
|
|
106
|
+
(0, import_vitest.expect)(preview.title).toBe("Update file");
|
|
107
|
+
(0, import_vitest.expect)(preview.filename).toBe(externalFile);
|
|
108
|
+
} else {
|
|
109
|
+
import_vitest.expect.fail("Expected diff preview");
|
|
110
|
+
}
|
|
111
|
+
} finally {
|
|
112
|
+
await fs.rm(externalDir, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
81
115
|
(0, import_vitest.it)("should succeed when file is not modified between preview and execute", async () => {
|
|
82
116
|
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
117
|
+
const previewFn = tool.presentation?.preview;
|
|
118
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
83
119
|
const testFile = path.join(tempDir, "test.txt");
|
|
84
120
|
await fs.writeFile(testFile, "hello world");
|
|
85
121
|
const toolCallId = "test-call-123";
|
|
@@ -89,7 +125,7 @@ function createToolContext(logger, overrides = {}) {
|
|
|
89
125
|
new_string: "universe"
|
|
90
126
|
};
|
|
91
127
|
const parsedInput = tool.inputSchema.parse(input);
|
|
92
|
-
const preview = await
|
|
128
|
+
const preview = await previewFn(
|
|
93
129
|
parsedInput,
|
|
94
130
|
createToolContext(mockLogger, { toolCallId })
|
|
95
131
|
);
|
|
@@ -105,6 +141,8 @@ function createToolContext(logger, overrides = {}) {
|
|
|
105
141
|
});
|
|
106
142
|
(0, import_vitest.it)("should fail when file is modified between preview and execute", async () => {
|
|
107
143
|
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
144
|
+
const previewFn = tool.presentation?.preview;
|
|
145
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
108
146
|
const testFile = path.join(tempDir, "test.txt");
|
|
109
147
|
await fs.writeFile(testFile, "hello world");
|
|
110
148
|
const toolCallId = "test-call-456";
|
|
@@ -114,7 +152,7 @@ function createToolContext(logger, overrides = {}) {
|
|
|
114
152
|
new_string: "universe"
|
|
115
153
|
};
|
|
116
154
|
const parsedInput = tool.inputSchema.parse(input);
|
|
117
|
-
const preview = await
|
|
155
|
+
const preview = await previewFn(
|
|
118
156
|
parsedInput,
|
|
119
157
|
createToolContext(mockLogger, { toolCallId })
|
|
120
158
|
);
|
|
@@ -134,6 +172,8 @@ function createToolContext(logger, overrides = {}) {
|
|
|
134
172
|
});
|
|
135
173
|
(0, import_vitest.it)("should detect file modification with correct error code", async () => {
|
|
136
174
|
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
175
|
+
const previewFn = tool.presentation?.preview;
|
|
176
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
137
177
|
const testFile = path.join(tempDir, "test.txt");
|
|
138
178
|
await fs.writeFile(testFile, "hello world");
|
|
139
179
|
const toolCallId = "test-call-789";
|
|
@@ -143,7 +183,7 @@ function createToolContext(logger, overrides = {}) {
|
|
|
143
183
|
new_string: "universe"
|
|
144
184
|
};
|
|
145
185
|
const parsedInput = tool.inputSchema.parse(input);
|
|
146
|
-
await
|
|
186
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
147
187
|
await fs.writeFile(testFile, "hello world modified");
|
|
148
188
|
try {
|
|
149
189
|
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
@@ -161,6 +201,8 @@ function createToolContext(logger, overrides = {}) {
|
|
|
161
201
|
});
|
|
162
202
|
(0, import_vitest.it)("should work without toolCallId (no modification check)", async () => {
|
|
163
203
|
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
204
|
+
const previewFn = tool.presentation?.preview;
|
|
205
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
164
206
|
const testFile = path.join(tempDir, "test.txt");
|
|
165
207
|
await fs.writeFile(testFile, "hello world");
|
|
166
208
|
const input = {
|
|
@@ -169,7 +211,7 @@ function createToolContext(logger, overrides = {}) {
|
|
|
169
211
|
new_string: "universe"
|
|
170
212
|
};
|
|
171
213
|
const parsedInput = tool.inputSchema.parse(input);
|
|
172
|
-
await
|
|
214
|
+
await previewFn(parsedInput, createToolContext(mockLogger));
|
|
173
215
|
await fs.writeFile(testFile, "hello world changed");
|
|
174
216
|
try {
|
|
175
217
|
await tool.execute(parsedInput, createToolContext(mockLogger));
|
|
@@ -182,6 +224,8 @@ function createToolContext(logger, overrides = {}) {
|
|
|
182
224
|
});
|
|
183
225
|
(0, import_vitest.it)("should clean up hash cache after successful execution", async () => {
|
|
184
226
|
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
227
|
+
const previewFn = tool.presentation?.preview;
|
|
228
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
185
229
|
const testFile = path.join(tempDir, "test.txt");
|
|
186
230
|
await fs.writeFile(testFile, "hello world");
|
|
187
231
|
const toolCallId = "test-call-cleanup";
|
|
@@ -191,7 +235,7 @@ function createToolContext(logger, overrides = {}) {
|
|
|
191
235
|
new_string: "universe"
|
|
192
236
|
};
|
|
193
237
|
const parsedInput = tool.inputSchema.parse(input);
|
|
194
|
-
await
|
|
238
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
195
239
|
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
196
240
|
const input2 = {
|
|
197
241
|
file_path: testFile,
|
|
@@ -199,10 +243,7 @@ function createToolContext(logger, overrides = {}) {
|
|
|
199
243
|
new_string: "galaxy"
|
|
200
244
|
};
|
|
201
245
|
const parsedInput2 = tool.inputSchema.parse(input2);
|
|
202
|
-
await
|
|
203
|
-
parsedInput2,
|
|
204
|
-
createToolContext(mockLogger, { toolCallId })
|
|
205
|
-
);
|
|
246
|
+
await previewFn(parsedInput2, createToolContext(mockLogger, { toolCallId }));
|
|
206
247
|
const result = await tool.execute(
|
|
207
248
|
parsedInput2,
|
|
208
249
|
createToolContext(mockLogger, { toolCallId })
|
|
@@ -213,6 +254,8 @@ function createToolContext(logger, overrides = {}) {
|
|
|
213
254
|
});
|
|
214
255
|
(0, import_vitest.it)("should clean up hash cache after failed execution", async () => {
|
|
215
256
|
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
257
|
+
const previewFn = tool.presentation?.preview;
|
|
258
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
216
259
|
const testFile = path.join(tempDir, "test.txt");
|
|
217
260
|
await fs.writeFile(testFile, "hello world");
|
|
218
261
|
const toolCallId = "test-call-fail-cleanup";
|
|
@@ -222,14 +265,14 @@ function createToolContext(logger, overrides = {}) {
|
|
|
222
265
|
new_string: "universe"
|
|
223
266
|
};
|
|
224
267
|
const parsedInput = tool.inputSchema.parse(input);
|
|
225
|
-
await
|
|
268
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
226
269
|
await fs.writeFile(testFile, "hello world modified");
|
|
227
270
|
try {
|
|
228
271
|
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
229
272
|
} catch {
|
|
230
273
|
}
|
|
231
274
|
await fs.writeFile(testFile, "hello world");
|
|
232
|
-
await
|
|
275
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
233
276
|
const result = await tool.execute(
|
|
234
277
|
parsedInput,
|
|
235
278
|
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,8 +56,43 @@ describe("edit_file tool", () => {
|
|
|
55
56
|
}
|
|
56
57
|
});
|
|
57
58
|
describe("File Modification Detection", () => {
|
|
59
|
+
it("should generate preview for files outside config-allowed roots (preview read only)", async () => {
|
|
60
|
+
const tool = createEditFileTool(async () => fileSystemService);
|
|
61
|
+
const previewFn = tool.presentation?.preview;
|
|
62
|
+
expect(previewFn).toBeDefined();
|
|
63
|
+
const rawExternalDir = await fs.mkdtemp(
|
|
64
|
+
path.join(os.tmpdir(), "dexto-edit-outside-allowed-")
|
|
65
|
+
);
|
|
66
|
+
const externalDir = await fs.realpath(rawExternalDir);
|
|
67
|
+
const externalFile = path.join(externalDir, "external.txt");
|
|
68
|
+
try {
|
|
69
|
+
await fs.writeFile(externalFile, "hello world");
|
|
70
|
+
const toolCallId = "preview-outside-roots";
|
|
71
|
+
const parsedInput = tool.inputSchema.parse({
|
|
72
|
+
file_path: externalFile,
|
|
73
|
+
old_string: "world",
|
|
74
|
+
new_string: "universe"
|
|
75
|
+
});
|
|
76
|
+
const preview = await previewFn(
|
|
77
|
+
parsedInput,
|
|
78
|
+
createToolContext(mockLogger, { toolCallId })
|
|
79
|
+
);
|
|
80
|
+
expect(preview).toBeDefined();
|
|
81
|
+
expect(preview?.type).toBe("diff");
|
|
82
|
+
if (preview?.type === "diff") {
|
|
83
|
+
expect(preview.title).toBe("Update file");
|
|
84
|
+
expect(preview.filename).toBe(externalFile);
|
|
85
|
+
} else {
|
|
86
|
+
expect.fail("Expected diff preview");
|
|
87
|
+
}
|
|
88
|
+
} finally {
|
|
89
|
+
await fs.rm(externalDir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
58
92
|
it("should succeed when file is not modified between preview and execute", async () => {
|
|
59
93
|
const tool = createEditFileTool(async () => fileSystemService);
|
|
94
|
+
const previewFn = tool.presentation?.preview;
|
|
95
|
+
expect(previewFn).toBeDefined();
|
|
60
96
|
const testFile = path.join(tempDir, "test.txt");
|
|
61
97
|
await fs.writeFile(testFile, "hello world");
|
|
62
98
|
const toolCallId = "test-call-123";
|
|
@@ -66,7 +102,7 @@ describe("edit_file tool", () => {
|
|
|
66
102
|
new_string: "universe"
|
|
67
103
|
};
|
|
68
104
|
const parsedInput = tool.inputSchema.parse(input);
|
|
69
|
-
const preview = await
|
|
105
|
+
const preview = await previewFn(
|
|
70
106
|
parsedInput,
|
|
71
107
|
createToolContext(mockLogger, { toolCallId })
|
|
72
108
|
);
|
|
@@ -82,6 +118,8 @@ describe("edit_file tool", () => {
|
|
|
82
118
|
});
|
|
83
119
|
it("should fail when file is modified between preview and execute", async () => {
|
|
84
120
|
const tool = createEditFileTool(async () => fileSystemService);
|
|
121
|
+
const previewFn = tool.presentation?.preview;
|
|
122
|
+
expect(previewFn).toBeDefined();
|
|
85
123
|
const testFile = path.join(tempDir, "test.txt");
|
|
86
124
|
await fs.writeFile(testFile, "hello world");
|
|
87
125
|
const toolCallId = "test-call-456";
|
|
@@ -91,7 +129,7 @@ describe("edit_file tool", () => {
|
|
|
91
129
|
new_string: "universe"
|
|
92
130
|
};
|
|
93
131
|
const parsedInput = tool.inputSchema.parse(input);
|
|
94
|
-
const preview = await
|
|
132
|
+
const preview = await previewFn(
|
|
95
133
|
parsedInput,
|
|
96
134
|
createToolContext(mockLogger, { toolCallId })
|
|
97
135
|
);
|
|
@@ -111,6 +149,8 @@ describe("edit_file tool", () => {
|
|
|
111
149
|
});
|
|
112
150
|
it("should detect file modification with correct error code", async () => {
|
|
113
151
|
const tool = createEditFileTool(async () => fileSystemService);
|
|
152
|
+
const previewFn = tool.presentation?.preview;
|
|
153
|
+
expect(previewFn).toBeDefined();
|
|
114
154
|
const testFile = path.join(tempDir, "test.txt");
|
|
115
155
|
await fs.writeFile(testFile, "hello world");
|
|
116
156
|
const toolCallId = "test-call-789";
|
|
@@ -120,7 +160,7 @@ describe("edit_file tool", () => {
|
|
|
120
160
|
new_string: "universe"
|
|
121
161
|
};
|
|
122
162
|
const parsedInput = tool.inputSchema.parse(input);
|
|
123
|
-
await
|
|
163
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
124
164
|
await fs.writeFile(testFile, "hello world modified");
|
|
125
165
|
try {
|
|
126
166
|
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
@@ -138,6 +178,8 @@ describe("edit_file tool", () => {
|
|
|
138
178
|
});
|
|
139
179
|
it("should work without toolCallId (no modification check)", async () => {
|
|
140
180
|
const tool = createEditFileTool(async () => fileSystemService);
|
|
181
|
+
const previewFn = tool.presentation?.preview;
|
|
182
|
+
expect(previewFn).toBeDefined();
|
|
141
183
|
const testFile = path.join(tempDir, "test.txt");
|
|
142
184
|
await fs.writeFile(testFile, "hello world");
|
|
143
185
|
const input = {
|
|
@@ -146,7 +188,7 @@ describe("edit_file tool", () => {
|
|
|
146
188
|
new_string: "universe"
|
|
147
189
|
};
|
|
148
190
|
const parsedInput = tool.inputSchema.parse(input);
|
|
149
|
-
await
|
|
191
|
+
await previewFn(parsedInput, createToolContext(mockLogger));
|
|
150
192
|
await fs.writeFile(testFile, "hello world changed");
|
|
151
193
|
try {
|
|
152
194
|
await tool.execute(parsedInput, createToolContext(mockLogger));
|
|
@@ -159,6 +201,8 @@ describe("edit_file tool", () => {
|
|
|
159
201
|
});
|
|
160
202
|
it("should clean up hash cache after successful execution", async () => {
|
|
161
203
|
const tool = createEditFileTool(async () => fileSystemService);
|
|
204
|
+
const previewFn = tool.presentation?.preview;
|
|
205
|
+
expect(previewFn).toBeDefined();
|
|
162
206
|
const testFile = path.join(tempDir, "test.txt");
|
|
163
207
|
await fs.writeFile(testFile, "hello world");
|
|
164
208
|
const toolCallId = "test-call-cleanup";
|
|
@@ -168,7 +212,7 @@ describe("edit_file tool", () => {
|
|
|
168
212
|
new_string: "universe"
|
|
169
213
|
};
|
|
170
214
|
const parsedInput = tool.inputSchema.parse(input);
|
|
171
|
-
await
|
|
215
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
172
216
|
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
173
217
|
const input2 = {
|
|
174
218
|
file_path: testFile,
|
|
@@ -176,10 +220,7 @@ describe("edit_file tool", () => {
|
|
|
176
220
|
new_string: "galaxy"
|
|
177
221
|
};
|
|
178
222
|
const parsedInput2 = tool.inputSchema.parse(input2);
|
|
179
|
-
await
|
|
180
|
-
parsedInput2,
|
|
181
|
-
createToolContext(mockLogger, { toolCallId })
|
|
182
|
-
);
|
|
223
|
+
await previewFn(parsedInput2, createToolContext(mockLogger, { toolCallId }));
|
|
183
224
|
const result = await tool.execute(
|
|
184
225
|
parsedInput2,
|
|
185
226
|
createToolContext(mockLogger, { toolCallId })
|
|
@@ -190,6 +231,8 @@ describe("edit_file tool", () => {
|
|
|
190
231
|
});
|
|
191
232
|
it("should clean up hash cache after failed execution", async () => {
|
|
192
233
|
const tool = createEditFileTool(async () => fileSystemService);
|
|
234
|
+
const previewFn = tool.presentation?.preview;
|
|
235
|
+
expect(previewFn).toBeDefined();
|
|
193
236
|
const testFile = path.join(tempDir, "test.txt");
|
|
194
237
|
await fs.writeFile(testFile, "hello world");
|
|
195
238
|
const toolCallId = "test-call-fail-cleanup";
|
|
@@ -199,14 +242,14 @@ describe("edit_file tool", () => {
|
|
|
199
242
|
new_string: "universe"
|
|
200
243
|
};
|
|
201
244
|
const parsedInput = tool.inputSchema.parse(input);
|
|
202
|
-
await
|
|
245
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
203
246
|
await fs.writeFile(testFile, "hello world modified");
|
|
204
247
|
try {
|
|
205
248
|
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
206
249
|
} catch {
|
|
207
250
|
}
|
|
208
251
|
await fs.writeFile(testFile, "hello world");
|
|
209
|
-
await
|
|
252
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
210
253
|
const result = await tool.execute(
|
|
211
254
|
parsedInput,
|
|
212
255
|
createToolContext(mockLogger, { toolCallId })
|
package/dist/error-codes.cjs
CHANGED
|
@@ -34,7 +34,11 @@ var FileSystemErrorCode = /* @__PURE__ */ ((FileSystemErrorCode2) => {
|
|
|
34
34
|
FileSystemErrorCode2["FILE_TOO_LARGE"] = "FILESYSTEM_FILE_TOO_LARGE";
|
|
35
35
|
FileSystemErrorCode2["TOO_MANY_RESULTS"] = "FILESYSTEM_TOO_MANY_RESULTS";
|
|
36
36
|
FileSystemErrorCode2["READ_FAILED"] = "FILESYSTEM_READ_FAILED";
|
|
37
|
+
FileSystemErrorCode2["LIST_FAILED"] = "FILESYSTEM_LIST_FAILED";
|
|
37
38
|
FileSystemErrorCode2["WRITE_FAILED"] = "FILESYSTEM_WRITE_FAILED";
|
|
39
|
+
FileSystemErrorCode2["CREATE_DIR_FAILED"] = "FILESYSTEM_CREATE_DIR_FAILED";
|
|
40
|
+
FileSystemErrorCode2["DELETE_FAILED"] = "FILESYSTEM_DELETE_FAILED";
|
|
41
|
+
FileSystemErrorCode2["RENAME_FAILED"] = "FILESYSTEM_RENAME_FAILED";
|
|
38
42
|
FileSystemErrorCode2["BACKUP_FAILED"] = "FILESYSTEM_BACKUP_FAILED";
|
|
39
43
|
FileSystemErrorCode2["EDIT_FAILED"] = "FILESYSTEM_EDIT_FAILED";
|
|
40
44
|
FileSystemErrorCode2["STRING_NOT_UNIQUE"] = "FILESYSTEM_STRING_NOT_UNIQUE";
|