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