@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.
Files changed (73) hide show
  1. package/dist/directory-approval.cjs +44 -40
  2. package/dist/directory-approval.d.ts +8 -4
  3. package/dist/directory-approval.d.ts.map +1 -1
  4. package/dist/directory-approval.integration.test.cjs +107 -356
  5. package/dist/directory-approval.integration.test.d.ts +6 -6
  6. package/dist/directory-approval.integration.test.js +109 -360
  7. package/dist/directory-approval.js +45 -41
  8. package/dist/edit-file-tool.cjs +69 -47
  9. package/dist/edit-file-tool.d.ts.map +1 -1
  10. package/dist/edit-file-tool.js +77 -48
  11. package/dist/edit-file-tool.test.cjs +54 -11
  12. package/dist/edit-file-tool.test.js +54 -11
  13. package/dist/error-codes.cjs +4 -0
  14. package/dist/error-codes.d.ts +4 -0
  15. package/dist/error-codes.d.ts.map +1 -1
  16. package/dist/error-codes.js +4 -0
  17. package/dist/errors.cjs +48 -0
  18. package/dist/errors.d.ts +16 -0
  19. package/dist/errors.d.ts.map +1 -1
  20. package/dist/errors.js +48 -0
  21. package/dist/filesystem-service.cjs +307 -9
  22. package/dist/filesystem-service.d.ts +28 -1
  23. package/dist/filesystem-service.d.ts.map +1 -1
  24. package/dist/filesystem-service.js +308 -10
  25. package/dist/glob-files-tool.cjs +12 -1
  26. package/dist/glob-files-tool.d.ts.map +1 -1
  27. package/dist/glob-files-tool.js +13 -2
  28. package/dist/grep-content-tool.cjs +13 -1
  29. package/dist/grep-content-tool.d.ts.map +1 -1
  30. package/dist/grep-content-tool.js +14 -2
  31. package/dist/index.cjs +3 -0
  32. package/dist/index.d.cts +852 -16
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -0
  36. package/dist/path-validator.cjs +28 -2
  37. package/dist/path-validator.d.ts +14 -0
  38. package/dist/path-validator.d.ts.map +1 -1
  39. package/dist/path-validator.js +28 -2
  40. package/dist/read-file-tool.cjs +7 -1
  41. package/dist/read-file-tool.d.ts.map +1 -1
  42. package/dist/read-file-tool.js +8 -2
  43. package/dist/tool-factory.cjs +21 -0
  44. package/dist/tool-factory.d.ts.map +1 -1
  45. package/dist/tool-factory.js +21 -0
  46. package/dist/types.d.ts +65 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/write-file-tool.cjs +60 -38
  49. package/dist/write-file-tool.d.ts +1 -1
  50. package/dist/write-file-tool.d.ts.map +1 -1
  51. package/dist/write-file-tool.js +67 -39
  52. package/dist/write-file-tool.test.cjs +75 -13
  53. package/dist/write-file-tool.test.js +75 -13
  54. package/package.json +6 -6
  55. package/dist/directory-approval.d.cts +0 -22
  56. package/dist/directory-approval.integration.test.d.cts +0 -2
  57. package/dist/edit-file-tool.d.cts +0 -34
  58. package/dist/edit-file-tool.test.d.cts +0 -2
  59. package/dist/error-codes.d.cts +0 -32
  60. package/dist/errors.d.cts +0 -112
  61. package/dist/file-tool-types.d.cts +0 -18
  62. package/dist/filesystem-service.d.cts +0 -117
  63. package/dist/filesystem-service.test.d.cts +0 -2
  64. package/dist/glob-files-tool.d.cts +0 -31
  65. package/dist/grep-content-tool.d.cts +0 -40
  66. package/dist/path-validator.d.cts +0 -97
  67. package/dist/path-validator.test.d.cts +0 -2
  68. package/dist/read-file-tool.d.cts +0 -31
  69. package/dist/tool-factory-config.d.cts +0 -63
  70. package/dist/tool-factory.d.cts +0 -7
  71. package/dist/types.d.cts +0 -178
  72. package/dist/write-file-tool.d.cts +0 -34
  73. package/dist/write-file-tool.test.d.cts +0 -2
@@ -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
- * Generate preview for approval UI - shows diff without modifying file
69
- * Throws ToolError.validationFailed() for validation errors (file not found, string not found)
70
- * Stores content hash for change detection in execute phase.
71
- */
72
- async generatePreview(input, context) {
73
- const { file_path, old_string, new_string, replace_all } = input;
74
- const resolvedFileSystemService = await getFileSystemService(context);
75
- const { path: resolvedPath } = (0, import_directory_approval.resolveFilePath)(
76
- resolvedFileSystemService.getWorkingDirectory(),
77
- file_path
78
- );
79
- try {
80
- const originalFile = await resolvedFileSystemService.readFile(resolvedPath);
81
- const originalContent = originalFile.content;
82
- if (context.toolCallId) {
83
- previewContentHashCache.set(
84
- context.toolCallId,
85
- computeContentHash(originalContent)
86
- );
87
- }
88
- if (!replace_all) {
89
- const occurrences = originalContent.split(old_string).length - 1;
90
- if (occurrences > 1) {
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 ${occurrences} times in file. Set replace_all=true to replace all, or provide more context to make old_string unique.`,
94
- { file_path: resolvedPath, occurrences }
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;AAGxB,OAAO,KAAK,EAAE,IAAI,EAAwB,MAAM,aAAa,CAAC;AAE9D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAkBpE,QAAA,MAAM,mBAAmB;;;;;;;;;;;;;;;EAaZ,CAAC;AAyBd;;GAEG;AACH,wBAAgB,kBAAkB,CAC9B,oBAAoB,EAAE,uBAAuB,GAC9C,IAAI,CAAC,OAAO,mBAAmB,CAAC,CAgKlC"}
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"}
@@ -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 { DextoRuntimeError, ToolError, ToolErrorCode, defineTool } from "@dexto/core";
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
- * Generate preview for approval UI - shows diff without modifying file
46
- * Throws ToolError.validationFailed() for validation errors (file not found, string not found)
47
- * Stores content hash for change detection in execute phase.
48
- */
49
- async generatePreview(input, context) {
50
- const { file_path, old_string, new_string, replace_all } = input;
51
- const resolvedFileSystemService = await getFileSystemService(context);
52
- const { path: resolvedPath } = resolveFilePath(
53
- resolvedFileSystemService.getWorkingDirectory(),
54
- file_path
55
- );
56
- try {
57
- const originalFile = await resolvedFileSystemService.readFile(resolvedPath);
58
- const originalContent = originalFile.content;
59
- if (context.toolCallId) {
60
- previewContentHashCache.set(
61
- context.toolCallId,
62
- computeContentHash(originalContent)
63
- );
64
- }
65
- if (!replace_all) {
66
- const occurrences = originalContent.split(old_string).length - 1;
67
- if (occurrences > 1) {
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 ${occurrences} times in file. Set replace_all=true to replace all, or provide more context to make old_string unique.`,
71
- { file_path: resolvedPath, occurrences }
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 tool.generatePreview(
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 tool.generatePreview(
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 tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
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 tool.generatePreview(parsedInput, createToolContext(mockLogger));
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 tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
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 tool.generatePreview(
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 tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
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 tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
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 tool.generatePreview(
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 tool.generatePreview(
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 tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
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 tool.generatePreview(parsedInput, createToolContext(mockLogger));
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 tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
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 tool.generatePreview(
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 tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
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 tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
252
+ await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
210
253
  const result = await tool.execute(
211
254
  parsedInput,
212
255
  createToolContext(mockLogger, { toolCallId })
@@ -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";