@dexto/tools-filesystem 1.5.2 → 1.5.4
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.integration.test.cjs +36 -32
- package/dist/directory-approval.integration.test.js +36 -32
- package/dist/edit-file-tool.cjs +43 -19
- package/dist/edit-file-tool.js +43 -19
- package/dist/edit-file-tool.test.cjs +203 -0
- package/dist/edit-file-tool.test.d.cts +2 -0
- package/dist/edit-file-tool.test.d.ts +2 -0
- package/dist/edit-file-tool.test.js +180 -0
- package/dist/filesystem-service.cjs +24 -14
- package/dist/filesystem-service.d.cts +8 -3
- package/dist/filesystem-service.d.ts +8 -3
- package/dist/filesystem-service.js +24 -14
- package/dist/filesystem-service.test.cjs +233 -0
- package/dist/filesystem-service.test.d.cts +2 -0
- package/dist/filesystem-service.test.d.ts +2 -0
- package/dist/filesystem-service.test.js +210 -0
- package/dist/glob-files-tool.cjs +56 -3
- package/dist/glob-files-tool.d.cts +4 -3
- package/dist/glob-files-tool.d.ts +4 -3
- package/dist/glob-files-tool.js +46 -3
- package/dist/grep-content-tool.cjs +55 -3
- package/dist/grep-content-tool.d.cts +4 -3
- package/dist/grep-content-tool.d.ts +4 -3
- package/dist/grep-content-tool.js +45 -3
- package/dist/path-validator.cjs +29 -20
- package/dist/path-validator.d.cts +9 -2
- package/dist/path-validator.d.ts +9 -2
- package/dist/path-validator.js +29 -20
- package/dist/path-validator.test.cjs +54 -48
- package/dist/path-validator.test.js +54 -48
- package/dist/read-file-tool.cjs +2 -2
- package/dist/read-file-tool.js +2 -2
- package/dist/tool-provider.cjs +22 -7
- package/dist/tool-provider.d.cts +4 -1
- package/dist/tool-provider.d.ts +4 -1
- package/dist/tool-provider.js +22 -7
- package/dist/types.d.cts +6 -0
- package/dist/types.d.ts +6 -0
- package/dist/write-file-tool.cjs +41 -7
- package/dist/write-file-tool.js +46 -8
- package/dist/write-file-tool.test.cjs +217 -0
- package/dist/write-file-tool.test.d.cts +2 -0
- package/dist/write-file-tool.test.d.ts +2 -0
- package/dist/write-file-tool.test.js +194 -0
- package/package.json +2 -2
package/dist/read-file-tool.js
CHANGED
|
@@ -17,10 +17,10 @@ function createReadFileTool(options) {
|
|
|
17
17
|
* Check if this read operation needs directory access approval.
|
|
18
18
|
* Returns custom approval request if the file is outside allowed paths.
|
|
19
19
|
*/
|
|
20
|
-
getApprovalOverride: (args) => {
|
|
20
|
+
getApprovalOverride: async (args) => {
|
|
21
21
|
const { file_path } = args;
|
|
22
22
|
if (!file_path) return null;
|
|
23
|
-
const isAllowed = fileSystemService.isPathWithinConfigAllowed(file_path);
|
|
23
|
+
const isAllowed = await fileSystemService.isPathWithinConfigAllowed(file_path);
|
|
24
24
|
if (isAllowed) {
|
|
25
25
|
return null;
|
|
26
26
|
}
|
package/dist/tool-provider.cjs
CHANGED
|
@@ -34,6 +34,13 @@ const DEFAULT_BLOCKED_EXTENSIONS = [".exe", ".dll", ".so"];
|
|
|
34
34
|
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
35
35
|
const DEFAULT_ENABLE_BACKUPS = false;
|
|
36
36
|
const DEFAULT_BACKUP_RETENTION_DAYS = 7;
|
|
37
|
+
const FILESYSTEM_TOOL_NAMES = [
|
|
38
|
+
"read_file",
|
|
39
|
+
"write_file",
|
|
40
|
+
"edit_file",
|
|
41
|
+
"glob_files",
|
|
42
|
+
"grep_content"
|
|
43
|
+
];
|
|
37
44
|
const FileSystemToolsConfigSchema = import_zod.z.object({
|
|
38
45
|
type: import_zod.z.literal("filesystem-tools"),
|
|
39
46
|
allowedPaths: import_zod.z.array(import_zod.z.string()).default(DEFAULT_ALLOWED_PATHS).describe("List of allowed base paths for file operations"),
|
|
@@ -47,6 +54,9 @@ const FileSystemToolsConfigSchema = import_zod.z.object({
|
|
|
47
54
|
backupPath: import_zod.z.string().optional().describe("Absolute path for storing file backups (if enableBackups is true)"),
|
|
48
55
|
backupRetentionDays: import_zod.z.number().int().positive().default(DEFAULT_BACKUP_RETENTION_DAYS).describe(
|
|
49
56
|
`Number of days to retain backup files (default: ${DEFAULT_BACKUP_RETENTION_DAYS})`
|
|
57
|
+
),
|
|
58
|
+
enabledTools: import_zod.z.array(import_zod.z.enum(FILESYSTEM_TOOL_NAMES)).optional().describe(
|
|
59
|
+
`Subset of tools to enable. If not specified, all tools are enabled. Available: ${FILESYSTEM_TOOL_NAMES.join(", ")}`
|
|
50
60
|
)
|
|
51
61
|
}).strict();
|
|
52
62
|
const fileSystemToolsProvider = {
|
|
@@ -88,13 +98,18 @@ const fileSystemToolsProvider = {
|
|
|
88
98
|
fileSystemService,
|
|
89
99
|
directoryApproval
|
|
90
100
|
};
|
|
91
|
-
|
|
92
|
-
(0, import_read_file_tool.createReadFileTool)(fileToolOptions),
|
|
93
|
-
(0, import_write_file_tool.createWriteFileTool)(fileToolOptions),
|
|
94
|
-
(0, import_edit_file_tool.createEditFileTool)(fileToolOptions),
|
|
95
|
-
(0, import_glob_files_tool.createGlobFilesTool)(
|
|
96
|
-
(0, import_grep_content_tool.createGrepContentTool)(
|
|
97
|
-
|
|
101
|
+
const toolCreators = {
|
|
102
|
+
read_file: () => (0, import_read_file_tool.createReadFileTool)(fileToolOptions),
|
|
103
|
+
write_file: () => (0, import_write_file_tool.createWriteFileTool)(fileToolOptions),
|
|
104
|
+
edit_file: () => (0, import_edit_file_tool.createEditFileTool)(fileToolOptions),
|
|
105
|
+
glob_files: () => (0, import_glob_files_tool.createGlobFilesTool)(fileToolOptions),
|
|
106
|
+
grep_content: () => (0, import_grep_content_tool.createGrepContentTool)(fileToolOptions)
|
|
107
|
+
};
|
|
108
|
+
const toolsToCreate = config.enabledTools ?? FILESYSTEM_TOOL_NAMES;
|
|
109
|
+
if (config.enabledTools) {
|
|
110
|
+
logger.debug(`Creating subset of filesystem tools: ${toolsToCreate.join(", ")}`);
|
|
111
|
+
}
|
|
112
|
+
return toolsToCreate.map((toolName) => toolCreators[toolName]());
|
|
98
113
|
},
|
|
99
114
|
metadata: {
|
|
100
115
|
displayName: "FileSystem Tools",
|
package/dist/tool-provider.d.cts
CHANGED
|
@@ -34,16 +34,18 @@ declare const FileSystemToolsConfigSchema: z.ZodObject<{
|
|
|
34
34
|
enableBackups: z.ZodDefault<z.ZodBoolean>;
|
|
35
35
|
backupPath: z.ZodOptional<z.ZodString>;
|
|
36
36
|
backupRetentionDays: z.ZodDefault<z.ZodNumber>;
|
|
37
|
+
enabledTools: z.ZodOptional<z.ZodArray<z.ZodEnum<["read_file", "write_file", "edit_file", "glob_files", "grep_content"]>, "many">>;
|
|
37
38
|
}, "strict", z.ZodTypeAny, {
|
|
38
|
-
type: "filesystem-tools";
|
|
39
39
|
allowedPaths: string[];
|
|
40
40
|
blockedExtensions: string[];
|
|
41
41
|
blockedPaths: string[];
|
|
42
42
|
maxFileSize: number;
|
|
43
43
|
enableBackups: boolean;
|
|
44
44
|
backupRetentionDays: number;
|
|
45
|
+
type: "filesystem-tools";
|
|
45
46
|
backupPath?: string | undefined;
|
|
46
47
|
workingDirectory?: string | undefined;
|
|
48
|
+
enabledTools?: ("edit_file" | "glob_files" | "grep_content" | "read_file" | "write_file")[] | undefined;
|
|
47
49
|
}, {
|
|
48
50
|
type: "filesystem-tools";
|
|
49
51
|
allowedPaths?: string[] | undefined;
|
|
@@ -54,6 +56,7 @@ declare const FileSystemToolsConfigSchema: z.ZodObject<{
|
|
|
54
56
|
enableBackups?: boolean | undefined;
|
|
55
57
|
backupRetentionDays?: number | undefined;
|
|
56
58
|
workingDirectory?: string | undefined;
|
|
59
|
+
enabledTools?: ("edit_file" | "glob_files" | "grep_content" | "read_file" | "write_file")[] | undefined;
|
|
57
60
|
}>;
|
|
58
61
|
type FileSystemToolsConfig = z.output<typeof FileSystemToolsConfigSchema>;
|
|
59
62
|
/**
|
package/dist/tool-provider.d.ts
CHANGED
|
@@ -34,16 +34,18 @@ declare const FileSystemToolsConfigSchema: z.ZodObject<{
|
|
|
34
34
|
enableBackups: z.ZodDefault<z.ZodBoolean>;
|
|
35
35
|
backupPath: z.ZodOptional<z.ZodString>;
|
|
36
36
|
backupRetentionDays: z.ZodDefault<z.ZodNumber>;
|
|
37
|
+
enabledTools: z.ZodOptional<z.ZodArray<z.ZodEnum<["read_file", "write_file", "edit_file", "glob_files", "grep_content"]>, "many">>;
|
|
37
38
|
}, "strict", z.ZodTypeAny, {
|
|
38
|
-
type: "filesystem-tools";
|
|
39
39
|
allowedPaths: string[];
|
|
40
40
|
blockedExtensions: string[];
|
|
41
41
|
blockedPaths: string[];
|
|
42
42
|
maxFileSize: number;
|
|
43
43
|
enableBackups: boolean;
|
|
44
44
|
backupRetentionDays: number;
|
|
45
|
+
type: "filesystem-tools";
|
|
45
46
|
backupPath?: string | undefined;
|
|
46
47
|
workingDirectory?: string | undefined;
|
|
48
|
+
enabledTools?: ("edit_file" | "glob_files" | "grep_content" | "read_file" | "write_file")[] | undefined;
|
|
47
49
|
}, {
|
|
48
50
|
type: "filesystem-tools";
|
|
49
51
|
allowedPaths?: string[] | undefined;
|
|
@@ -54,6 +56,7 @@ declare const FileSystemToolsConfigSchema: z.ZodObject<{
|
|
|
54
56
|
enableBackups?: boolean | undefined;
|
|
55
57
|
backupRetentionDays?: number | undefined;
|
|
56
58
|
workingDirectory?: string | undefined;
|
|
59
|
+
enabledTools?: ("edit_file" | "glob_files" | "grep_content" | "read_file" | "write_file")[] | undefined;
|
|
57
60
|
}>;
|
|
58
61
|
type FileSystemToolsConfig = z.output<typeof FileSystemToolsConfigSchema>;
|
|
59
62
|
/**
|
package/dist/tool-provider.js
CHANGED
|
@@ -11,6 +11,13 @@ const DEFAULT_BLOCKED_EXTENSIONS = [".exe", ".dll", ".so"];
|
|
|
11
11
|
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
12
12
|
const DEFAULT_ENABLE_BACKUPS = false;
|
|
13
13
|
const DEFAULT_BACKUP_RETENTION_DAYS = 7;
|
|
14
|
+
const FILESYSTEM_TOOL_NAMES = [
|
|
15
|
+
"read_file",
|
|
16
|
+
"write_file",
|
|
17
|
+
"edit_file",
|
|
18
|
+
"glob_files",
|
|
19
|
+
"grep_content"
|
|
20
|
+
];
|
|
14
21
|
const FileSystemToolsConfigSchema = z.object({
|
|
15
22
|
type: z.literal("filesystem-tools"),
|
|
16
23
|
allowedPaths: z.array(z.string()).default(DEFAULT_ALLOWED_PATHS).describe("List of allowed base paths for file operations"),
|
|
@@ -24,6 +31,9 @@ const FileSystemToolsConfigSchema = z.object({
|
|
|
24
31
|
backupPath: z.string().optional().describe("Absolute path for storing file backups (if enableBackups is true)"),
|
|
25
32
|
backupRetentionDays: z.number().int().positive().default(DEFAULT_BACKUP_RETENTION_DAYS).describe(
|
|
26
33
|
`Number of days to retain backup files (default: ${DEFAULT_BACKUP_RETENTION_DAYS})`
|
|
34
|
+
),
|
|
35
|
+
enabledTools: z.array(z.enum(FILESYSTEM_TOOL_NAMES)).optional().describe(
|
|
36
|
+
`Subset of tools to enable. If not specified, all tools are enabled. Available: ${FILESYSTEM_TOOL_NAMES.join(", ")}`
|
|
27
37
|
)
|
|
28
38
|
}).strict();
|
|
29
39
|
const fileSystemToolsProvider = {
|
|
@@ -65,13 +75,18 @@ const fileSystemToolsProvider = {
|
|
|
65
75
|
fileSystemService,
|
|
66
76
|
directoryApproval
|
|
67
77
|
};
|
|
68
|
-
|
|
69
|
-
createReadFileTool(fileToolOptions),
|
|
70
|
-
createWriteFileTool(fileToolOptions),
|
|
71
|
-
createEditFileTool(fileToolOptions),
|
|
72
|
-
createGlobFilesTool(
|
|
73
|
-
createGrepContentTool(
|
|
74
|
-
|
|
78
|
+
const toolCreators = {
|
|
79
|
+
read_file: () => createReadFileTool(fileToolOptions),
|
|
80
|
+
write_file: () => createWriteFileTool(fileToolOptions),
|
|
81
|
+
edit_file: () => createEditFileTool(fileToolOptions),
|
|
82
|
+
glob_files: () => createGlobFilesTool(fileToolOptions),
|
|
83
|
+
grep_content: () => createGrepContentTool(fileToolOptions)
|
|
84
|
+
};
|
|
85
|
+
const toolsToCreate = config.enabledTools ?? FILESYSTEM_TOOL_NAMES;
|
|
86
|
+
if (config.enabledTools) {
|
|
87
|
+
logger.debug(`Creating subset of filesystem tools: ${toolsToCreate.join(", ")}`);
|
|
88
|
+
}
|
|
89
|
+
return toolsToCreate.map((toolName) => toolCreators[toolName]());
|
|
75
90
|
},
|
|
76
91
|
metadata: {
|
|
77
92
|
displayName: "FileSystem Tools",
|
package/dist/types.d.cts
CHANGED
|
@@ -112,6 +112,8 @@ interface WriteResult {
|
|
|
112
112
|
path: string;
|
|
113
113
|
bytesWritten: number;
|
|
114
114
|
backupPath?: string | undefined;
|
|
115
|
+
/** Original content if file was overwritten (undefined for new files) */
|
|
116
|
+
originalContent?: string | undefined;
|
|
115
117
|
}
|
|
116
118
|
/**
|
|
117
119
|
* Edit operation
|
|
@@ -138,6 +140,10 @@ interface EditResult {
|
|
|
138
140
|
path: string;
|
|
139
141
|
changesCount: number;
|
|
140
142
|
backupPath?: string | undefined;
|
|
143
|
+
/** Original content before edit (for diff generation) */
|
|
144
|
+
originalContent: string;
|
|
145
|
+
/** New content after edit (for diff generation) */
|
|
146
|
+
newContent: string;
|
|
141
147
|
}
|
|
142
148
|
/**
|
|
143
149
|
* Path validation result
|
package/dist/types.d.ts
CHANGED
|
@@ -112,6 +112,8 @@ interface WriteResult {
|
|
|
112
112
|
path: string;
|
|
113
113
|
bytesWritten: number;
|
|
114
114
|
backupPath?: string | undefined;
|
|
115
|
+
/** Original content if file was overwritten (undefined for new files) */
|
|
116
|
+
originalContent?: string | undefined;
|
|
115
117
|
}
|
|
116
118
|
/**
|
|
117
119
|
* Edit operation
|
|
@@ -138,6 +140,10 @@ interface EditResult {
|
|
|
138
140
|
path: string;
|
|
139
141
|
changesCount: number;
|
|
140
142
|
backupPath?: string | undefined;
|
|
143
|
+
/** Original content before edit (for diff generation) */
|
|
144
|
+
originalContent: string;
|
|
145
|
+
/** New content after edit (for diff generation) */
|
|
146
|
+
newContent: string;
|
|
141
147
|
}
|
|
142
148
|
/**
|
|
143
149
|
* Path validation result
|
package/dist/write-file-tool.cjs
CHANGED
|
@@ -32,10 +32,16 @@ __export(write_file_tool_exports, {
|
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(write_file_tool_exports);
|
|
34
34
|
var path = __toESM(require("node:path"), 1);
|
|
35
|
+
var import_node_crypto = require("node:crypto");
|
|
35
36
|
var import_zod = require("zod");
|
|
36
37
|
var import_diff = require("diff");
|
|
37
38
|
var import_core = require("@dexto/core");
|
|
38
39
|
var import_error_codes = require("./error-codes.js");
|
|
40
|
+
const previewContentHashCache = /* @__PURE__ */ new Map();
|
|
41
|
+
const FILE_NOT_EXISTS_MARKER = null;
|
|
42
|
+
function computeContentHash(content) {
|
|
43
|
+
return (0, import_node_crypto.createHash)("sha256").update(content, "utf8").digest("hex");
|
|
44
|
+
}
|
|
39
45
|
const WriteFileInputSchema = import_zod.z.object({
|
|
40
46
|
file_path: import_zod.z.string().describe("Absolute path where the file should be written"),
|
|
41
47
|
content: import_zod.z.string().describe("Content to write to the file"),
|
|
@@ -67,10 +73,10 @@ function createWriteFileTool(options) {
|
|
|
67
73
|
* Check if this write operation needs directory access approval.
|
|
68
74
|
* Returns custom approval request if the file is outside allowed paths.
|
|
69
75
|
*/
|
|
70
|
-
getApprovalOverride: (args) => {
|
|
76
|
+
getApprovalOverride: async (args) => {
|
|
71
77
|
const { file_path } = args;
|
|
72
78
|
if (!file_path) return null;
|
|
73
|
-
const isAllowed = fileSystemService.isPathWithinConfigAllowed(file_path);
|
|
79
|
+
const isAllowed = await fileSystemService.isPathWithinConfigAllowed(file_path);
|
|
74
80
|
if (isAllowed) {
|
|
75
81
|
return null;
|
|
76
82
|
}
|
|
@@ -105,15 +111,25 @@ function createWriteFileTool(options) {
|
|
|
105
111
|
},
|
|
106
112
|
/**
|
|
107
113
|
* Generate preview for approval UI - shows diff or file creation info
|
|
114
|
+
* Stores content hash for change detection in execute phase.
|
|
108
115
|
*/
|
|
109
|
-
generatePreview: async (input,
|
|
116
|
+
generatePreview: async (input, context) => {
|
|
110
117
|
const { file_path, content } = input;
|
|
111
118
|
try {
|
|
112
119
|
const originalFile = await fileSystemService.readFile(file_path);
|
|
113
120
|
const originalContent = originalFile.content;
|
|
121
|
+
if (context?.toolCallId) {
|
|
122
|
+
previewContentHashCache.set(
|
|
123
|
+
context.toolCallId,
|
|
124
|
+
computeContentHash(originalContent)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
114
127
|
return generateDiffPreview(file_path, originalContent, content);
|
|
115
128
|
} catch (error) {
|
|
116
129
|
if (error instanceof import_core.DextoRuntimeError && error.code === import_error_codes.FileSystemErrorCode.FILE_NOT_FOUND) {
|
|
130
|
+
if (context?.toolCallId) {
|
|
131
|
+
previewContentHashCache.set(context.toolCallId, FILE_NOT_EXISTS_MARKER);
|
|
132
|
+
}
|
|
117
133
|
const lineCount = content.split("\n").length;
|
|
118
134
|
const preview = {
|
|
119
135
|
type: "file",
|
|
@@ -129,24 +145,42 @@ function createWriteFileTool(options) {
|
|
|
129
145
|
throw error;
|
|
130
146
|
}
|
|
131
147
|
},
|
|
132
|
-
execute: async (input,
|
|
148
|
+
execute: async (input, context) => {
|
|
133
149
|
const { file_path, content, create_dirs, encoding } = input;
|
|
134
150
|
let originalContent = null;
|
|
151
|
+
let fileExistsNow = false;
|
|
135
152
|
try {
|
|
136
153
|
const originalFile = await fileSystemService.readFile(file_path);
|
|
137
154
|
originalContent = originalFile.content;
|
|
155
|
+
fileExistsNow = true;
|
|
138
156
|
} catch (error) {
|
|
139
157
|
if (error instanceof import_core.DextoRuntimeError && error.code === import_error_codes.FileSystemErrorCode.FILE_NOT_FOUND) {
|
|
140
158
|
originalContent = null;
|
|
159
|
+
fileExistsNow = false;
|
|
141
160
|
} else {
|
|
142
161
|
throw error;
|
|
143
162
|
}
|
|
144
163
|
}
|
|
164
|
+
if (context?.toolCallId && previewContentHashCache.has(context.toolCallId)) {
|
|
165
|
+
const expectedHash = previewContentHashCache.get(context.toolCallId);
|
|
166
|
+
previewContentHashCache.delete(context.toolCallId);
|
|
167
|
+
if (expectedHash === FILE_NOT_EXISTS_MARKER) {
|
|
168
|
+
if (fileExistsNow) {
|
|
169
|
+
throw import_core.ToolError.fileModifiedSincePreview("write_file", file_path);
|
|
170
|
+
}
|
|
171
|
+
} else if (expectedHash !== null) {
|
|
172
|
+
if (!fileExistsNow) {
|
|
173
|
+
throw import_core.ToolError.fileModifiedSincePreview("write_file", file_path);
|
|
174
|
+
}
|
|
175
|
+
const currentHash = computeContentHash(originalContent);
|
|
176
|
+
if (expectedHash !== currentHash) {
|
|
177
|
+
throw import_core.ToolError.fileModifiedSincePreview("write_file", file_path);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
145
181
|
const result = await fileSystemService.writeFile(file_path, content, {
|
|
146
182
|
createDirs: create_dirs,
|
|
147
|
-
encoding
|
|
148
|
-
backup: true
|
|
149
|
-
// Always create backup for internal tools
|
|
183
|
+
encoding
|
|
150
184
|
});
|
|
151
185
|
let _display;
|
|
152
186
|
if (originalContent === null) {
|
package/dist/write-file-tool.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { createPatch } from "diff";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DextoRuntimeError,
|
|
7
|
+
ApprovalType,
|
|
8
|
+
ToolError
|
|
9
|
+
} from "@dexto/core";
|
|
5
10
|
import { FileSystemErrorCode } from "./error-codes.js";
|
|
11
|
+
const previewContentHashCache = /* @__PURE__ */ new Map();
|
|
12
|
+
const FILE_NOT_EXISTS_MARKER = null;
|
|
13
|
+
function computeContentHash(content) {
|
|
14
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
15
|
+
}
|
|
6
16
|
const WriteFileInputSchema = z.object({
|
|
7
17
|
file_path: z.string().describe("Absolute path where the file should be written"),
|
|
8
18
|
content: z.string().describe("Content to write to the file"),
|
|
@@ -34,10 +44,10 @@ function createWriteFileTool(options) {
|
|
|
34
44
|
* Check if this write operation needs directory access approval.
|
|
35
45
|
* Returns custom approval request if the file is outside allowed paths.
|
|
36
46
|
*/
|
|
37
|
-
getApprovalOverride: (args) => {
|
|
47
|
+
getApprovalOverride: async (args) => {
|
|
38
48
|
const { file_path } = args;
|
|
39
49
|
if (!file_path) return null;
|
|
40
|
-
const isAllowed = fileSystemService.isPathWithinConfigAllowed(file_path);
|
|
50
|
+
const isAllowed = await fileSystemService.isPathWithinConfigAllowed(file_path);
|
|
41
51
|
if (isAllowed) {
|
|
42
52
|
return null;
|
|
43
53
|
}
|
|
@@ -72,15 +82,25 @@ function createWriteFileTool(options) {
|
|
|
72
82
|
},
|
|
73
83
|
/**
|
|
74
84
|
* Generate preview for approval UI - shows diff or file creation info
|
|
85
|
+
* Stores content hash for change detection in execute phase.
|
|
75
86
|
*/
|
|
76
|
-
generatePreview: async (input,
|
|
87
|
+
generatePreview: async (input, context) => {
|
|
77
88
|
const { file_path, content } = input;
|
|
78
89
|
try {
|
|
79
90
|
const originalFile = await fileSystemService.readFile(file_path);
|
|
80
91
|
const originalContent = originalFile.content;
|
|
92
|
+
if (context?.toolCallId) {
|
|
93
|
+
previewContentHashCache.set(
|
|
94
|
+
context.toolCallId,
|
|
95
|
+
computeContentHash(originalContent)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
81
98
|
return generateDiffPreview(file_path, originalContent, content);
|
|
82
99
|
} catch (error) {
|
|
83
100
|
if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.FILE_NOT_FOUND) {
|
|
101
|
+
if (context?.toolCallId) {
|
|
102
|
+
previewContentHashCache.set(context.toolCallId, FILE_NOT_EXISTS_MARKER);
|
|
103
|
+
}
|
|
84
104
|
const lineCount = content.split("\n").length;
|
|
85
105
|
const preview = {
|
|
86
106
|
type: "file",
|
|
@@ -96,24 +116,42 @@ function createWriteFileTool(options) {
|
|
|
96
116
|
throw error;
|
|
97
117
|
}
|
|
98
118
|
},
|
|
99
|
-
execute: async (input,
|
|
119
|
+
execute: async (input, context) => {
|
|
100
120
|
const { file_path, content, create_dirs, encoding } = input;
|
|
101
121
|
let originalContent = null;
|
|
122
|
+
let fileExistsNow = false;
|
|
102
123
|
try {
|
|
103
124
|
const originalFile = await fileSystemService.readFile(file_path);
|
|
104
125
|
originalContent = originalFile.content;
|
|
126
|
+
fileExistsNow = true;
|
|
105
127
|
} catch (error) {
|
|
106
128
|
if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.FILE_NOT_FOUND) {
|
|
107
129
|
originalContent = null;
|
|
130
|
+
fileExistsNow = false;
|
|
108
131
|
} else {
|
|
109
132
|
throw error;
|
|
110
133
|
}
|
|
111
134
|
}
|
|
135
|
+
if (context?.toolCallId && previewContentHashCache.has(context.toolCallId)) {
|
|
136
|
+
const expectedHash = previewContentHashCache.get(context.toolCallId);
|
|
137
|
+
previewContentHashCache.delete(context.toolCallId);
|
|
138
|
+
if (expectedHash === FILE_NOT_EXISTS_MARKER) {
|
|
139
|
+
if (fileExistsNow) {
|
|
140
|
+
throw ToolError.fileModifiedSincePreview("write_file", file_path);
|
|
141
|
+
}
|
|
142
|
+
} else if (expectedHash !== null) {
|
|
143
|
+
if (!fileExistsNow) {
|
|
144
|
+
throw ToolError.fileModifiedSincePreview("write_file", file_path);
|
|
145
|
+
}
|
|
146
|
+
const currentHash = computeContentHash(originalContent);
|
|
147
|
+
if (expectedHash !== currentHash) {
|
|
148
|
+
throw ToolError.fileModifiedSincePreview("write_file", file_path);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
112
152
|
const result = await fileSystemService.writeFile(file_path, content, {
|
|
113
153
|
createDirs: create_dirs,
|
|
114
|
-
encoding
|
|
115
|
-
backup: true
|
|
116
|
-
// Always create backup for internal tools
|
|
154
|
+
encoding
|
|
117
155
|
});
|
|
118
156
|
let _display;
|
|
119
157
|
if (originalContent === null) {
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
var import_vitest = require("vitest");
|
|
25
|
+
var path = __toESM(require("node:path"), 1);
|
|
26
|
+
var fs = __toESM(require("node:fs/promises"), 1);
|
|
27
|
+
var os = __toESM(require("node:os"), 1);
|
|
28
|
+
var import_write_file_tool = require("./write-file-tool.js");
|
|
29
|
+
var import_filesystem_service = require("./filesystem-service.js");
|
|
30
|
+
var import_core = require("@dexto/core");
|
|
31
|
+
var import_core2 = require("@dexto/core");
|
|
32
|
+
const createMockLogger = () => ({
|
|
33
|
+
debug: import_vitest.vi.fn(),
|
|
34
|
+
info: import_vitest.vi.fn(),
|
|
35
|
+
warn: import_vitest.vi.fn(),
|
|
36
|
+
error: import_vitest.vi.fn(),
|
|
37
|
+
createChild: import_vitest.vi.fn().mockReturnThis()
|
|
38
|
+
});
|
|
39
|
+
(0, import_vitest.describe)("write_file tool", () => {
|
|
40
|
+
let mockLogger;
|
|
41
|
+
let tempDir;
|
|
42
|
+
let fileSystemService;
|
|
43
|
+
(0, import_vitest.beforeEach)(async () => {
|
|
44
|
+
mockLogger = createMockLogger();
|
|
45
|
+
const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-write-test-"));
|
|
46
|
+
tempDir = await fs.realpath(rawTempDir);
|
|
47
|
+
fileSystemService = new import_filesystem_service.FileSystemService(
|
|
48
|
+
{
|
|
49
|
+
allowedPaths: [tempDir],
|
|
50
|
+
blockedPaths: [],
|
|
51
|
+
blockedExtensions: [],
|
|
52
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
53
|
+
workingDirectory: tempDir,
|
|
54
|
+
enableBackups: false,
|
|
55
|
+
backupRetentionDays: 7
|
|
56
|
+
},
|
|
57
|
+
mockLogger
|
|
58
|
+
);
|
|
59
|
+
await fileSystemService.initialize();
|
|
60
|
+
import_vitest.vi.clearAllMocks();
|
|
61
|
+
});
|
|
62
|
+
(0, import_vitest.afterEach)(async () => {
|
|
63
|
+
try {
|
|
64
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
(0, import_vitest.describe)("File Modification Detection - Existing Files", () => {
|
|
69
|
+
(0, import_vitest.it)("should succeed when existing file is not modified between preview and execute", async () => {
|
|
70
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)({ fileSystemService });
|
|
71
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
72
|
+
await fs.writeFile(testFile, "original content");
|
|
73
|
+
const toolCallId = "test-call-123";
|
|
74
|
+
const input = {
|
|
75
|
+
file_path: testFile,
|
|
76
|
+
content: "new content"
|
|
77
|
+
};
|
|
78
|
+
const preview = await tool.generatePreview(input, { toolCallId });
|
|
79
|
+
(0, import_vitest.expect)(preview).toBeDefined();
|
|
80
|
+
(0, import_vitest.expect)(preview?.type).toBe("diff");
|
|
81
|
+
const result = await tool.execute(input, { toolCallId });
|
|
82
|
+
(0, import_vitest.expect)(result.success).toBe(true);
|
|
83
|
+
(0, import_vitest.expect)(result.path).toBe(testFile);
|
|
84
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
85
|
+
(0, import_vitest.expect)(content).toBe("new content");
|
|
86
|
+
});
|
|
87
|
+
(0, import_vitest.it)("should fail when existing file is modified between preview and execute", async () => {
|
|
88
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)({ fileSystemService });
|
|
89
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
90
|
+
await fs.writeFile(testFile, "original content");
|
|
91
|
+
const toolCallId = "test-call-456";
|
|
92
|
+
const input = {
|
|
93
|
+
file_path: testFile,
|
|
94
|
+
content: "new content"
|
|
95
|
+
};
|
|
96
|
+
await tool.generatePreview(input, { toolCallId });
|
|
97
|
+
await fs.writeFile(testFile, "user modified this");
|
|
98
|
+
try {
|
|
99
|
+
await tool.execute(input, { toolCallId });
|
|
100
|
+
import_vitest.expect.fail("Should have thrown an error");
|
|
101
|
+
} catch (error) {
|
|
102
|
+
(0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
|
|
103
|
+
(0, import_vitest.expect)(error.code).toBe(
|
|
104
|
+
import_core.ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
108
|
+
(0, import_vitest.expect)(content).toBe("user modified this");
|
|
109
|
+
});
|
|
110
|
+
(0, import_vitest.it)("should fail when existing file is deleted between preview and execute", async () => {
|
|
111
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)({ fileSystemService });
|
|
112
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
113
|
+
await fs.writeFile(testFile, "original content");
|
|
114
|
+
const toolCallId = "test-call-deleted";
|
|
115
|
+
const input = {
|
|
116
|
+
file_path: testFile,
|
|
117
|
+
content: "new content"
|
|
118
|
+
};
|
|
119
|
+
await tool.generatePreview(input, { toolCallId });
|
|
120
|
+
await fs.unlink(testFile);
|
|
121
|
+
try {
|
|
122
|
+
await tool.execute(input, { toolCallId });
|
|
123
|
+
import_vitest.expect.fail("Should have thrown an error");
|
|
124
|
+
} catch (error) {
|
|
125
|
+
(0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
|
|
126
|
+
(0, import_vitest.expect)(error.code).toBe(
|
|
127
|
+
import_core.ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
(0, import_vitest.describe)("File Modification Detection - New Files", () => {
|
|
133
|
+
(0, import_vitest.it)("should succeed when creating new file that still does not exist", async () => {
|
|
134
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)({ fileSystemService });
|
|
135
|
+
const testFile = path.join(tempDir, "new-file.txt");
|
|
136
|
+
const toolCallId = "test-call-new";
|
|
137
|
+
const input = {
|
|
138
|
+
file_path: testFile,
|
|
139
|
+
content: "brand new content"
|
|
140
|
+
};
|
|
141
|
+
const preview = await tool.generatePreview(input, { toolCallId });
|
|
142
|
+
(0, import_vitest.expect)(preview).toBeDefined();
|
|
143
|
+
(0, import_vitest.expect)(preview?.type).toBe("file");
|
|
144
|
+
(0, import_vitest.expect)(preview.operation).toBe("create");
|
|
145
|
+
const result = await tool.execute(input, { toolCallId });
|
|
146
|
+
(0, import_vitest.expect)(result.success).toBe(true);
|
|
147
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
148
|
+
(0, import_vitest.expect)(content).toBe("brand new content");
|
|
149
|
+
});
|
|
150
|
+
(0, import_vitest.it)("should fail when file is created by someone else between preview and execute", async () => {
|
|
151
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)({ fileSystemService });
|
|
152
|
+
const testFile = path.join(tempDir, "race-condition.txt");
|
|
153
|
+
const toolCallId = "test-call-race";
|
|
154
|
+
const input = {
|
|
155
|
+
file_path: testFile,
|
|
156
|
+
content: "agent content"
|
|
157
|
+
};
|
|
158
|
+
const preview = await tool.generatePreview(input, { toolCallId });
|
|
159
|
+
(0, import_vitest.expect)(preview?.type).toBe("file");
|
|
160
|
+
await fs.writeFile(testFile, "someone else created this");
|
|
161
|
+
try {
|
|
162
|
+
await tool.execute(input, { toolCallId });
|
|
163
|
+
import_vitest.expect.fail("Should have thrown an error");
|
|
164
|
+
} catch (error) {
|
|
165
|
+
(0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
|
|
166
|
+
(0, import_vitest.expect)(error.code).toBe(
|
|
167
|
+
import_core.ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
171
|
+
(0, import_vitest.expect)(content).toBe("someone else created this");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
(0, import_vitest.describe)("Cache Cleanup", () => {
|
|
175
|
+
(0, import_vitest.it)("should clean up hash cache after successful execution", async () => {
|
|
176
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)({ fileSystemService });
|
|
177
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
178
|
+
await fs.writeFile(testFile, "original");
|
|
179
|
+
const toolCallId = "test-call-cleanup";
|
|
180
|
+
const input = {
|
|
181
|
+
file_path: testFile,
|
|
182
|
+
content: "first write"
|
|
183
|
+
};
|
|
184
|
+
await tool.generatePreview(input, { toolCallId });
|
|
185
|
+
await tool.execute(input, { toolCallId });
|
|
186
|
+
const input2 = {
|
|
187
|
+
file_path: testFile,
|
|
188
|
+
content: "second write"
|
|
189
|
+
};
|
|
190
|
+
await tool.generatePreview(input2, { toolCallId });
|
|
191
|
+
const result = await tool.execute(input2, { toolCallId });
|
|
192
|
+
(0, import_vitest.expect)(result.success).toBe(true);
|
|
193
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
194
|
+
(0, import_vitest.expect)(content).toBe("second write");
|
|
195
|
+
});
|
|
196
|
+
(0, import_vitest.it)("should clean up hash cache after failed execution", async () => {
|
|
197
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)({ fileSystemService });
|
|
198
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
199
|
+
await fs.writeFile(testFile, "original");
|
|
200
|
+
const toolCallId = "test-call-fail";
|
|
201
|
+
const input = {
|
|
202
|
+
file_path: testFile,
|
|
203
|
+
content: "new content"
|
|
204
|
+
};
|
|
205
|
+
await tool.generatePreview(input, { toolCallId });
|
|
206
|
+
await fs.writeFile(testFile, "modified");
|
|
207
|
+
try {
|
|
208
|
+
await tool.execute(input, { toolCallId });
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
await fs.writeFile(testFile, "reset content");
|
|
212
|
+
await tool.generatePreview(input, { toolCallId });
|
|
213
|
+
const result = await tool.execute(input, { toolCallId });
|
|
214
|
+
(0, import_vitest.expect)(result.success).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|