@dexto/tools-filesystem 1.5.8 → 1.6.1

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 (95) hide show
  1. package/dist/directory-approval.cjs +98 -0
  2. package/dist/directory-approval.d.ts +24 -0
  3. package/dist/directory-approval.d.ts.map +1 -0
  4. package/dist/directory-approval.integration.test.cjs +175 -390
  5. package/dist/directory-approval.integration.test.d.ts +14 -2
  6. package/dist/directory-approval.integration.test.d.ts.map +1 -0
  7. package/dist/directory-approval.integration.test.js +178 -390
  8. package/dist/directory-approval.js +63 -0
  9. package/dist/edit-file-tool.cjs +109 -120
  10. package/dist/edit-file-tool.d.ts +22 -9
  11. package/dist/edit-file-tool.d.ts.map +1 -0
  12. package/dist/edit-file-tool.js +116 -110
  13. package/dist/edit-file-tool.test.cjs +109 -29
  14. package/dist/edit-file-tool.test.d.ts +7 -2
  15. package/dist/edit-file-tool.test.d.ts.map +1 -0
  16. package/dist/edit-file-tool.test.js +109 -29
  17. package/dist/error-codes.cjs +4 -0
  18. package/dist/error-codes.d.ts +6 -3
  19. package/dist/error-codes.d.ts.map +1 -0
  20. package/dist/error-codes.js +4 -0
  21. package/dist/errors.cjs +48 -0
  22. package/dist/errors.d.ts +20 -7
  23. package/dist/errors.d.ts.map +1 -0
  24. package/dist/errors.js +48 -0
  25. package/dist/file-tool-types.d.ts +8 -40
  26. package/dist/file-tool-types.d.ts.map +1 -0
  27. package/dist/filesystem-service.cjs +325 -10
  28. package/dist/filesystem-service.d.ts +41 -12
  29. package/dist/filesystem-service.d.ts.map +1 -0
  30. package/dist/filesystem-service.js +326 -11
  31. package/dist/filesystem-service.test.cjs +10 -2
  32. package/dist/filesystem-service.test.d.ts +7 -2
  33. package/dist/filesystem-service.test.d.ts.map +1 -0
  34. package/dist/filesystem-service.test.js +10 -2
  35. package/dist/glob-files-tool.cjs +32 -46
  36. package/dist/glob-files-tool.d.ts +19 -9
  37. package/dist/glob-files-tool.d.ts.map +1 -0
  38. package/dist/glob-files-tool.js +33 -47
  39. package/dist/grep-content-tool.cjs +40 -45
  40. package/dist/grep-content-tool.d.ts +28 -9
  41. package/dist/grep-content-tool.d.ts.map +1 -0
  42. package/dist/grep-content-tool.js +41 -46
  43. package/dist/index.cjs +6 -3
  44. package/dist/index.d.cts +852 -14
  45. package/dist/index.d.ts +11 -5
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +4 -2
  48. package/dist/path-validator.cjs +28 -2
  49. package/dist/path-validator.d.ts +20 -9
  50. package/dist/path-validator.d.ts.map +1 -0
  51. package/dist/path-validator.js +28 -2
  52. package/dist/path-validator.test.d.ts +7 -2
  53. package/dist/path-validator.test.d.ts.map +1 -0
  54. package/dist/read-file-tool.cjs +26 -59
  55. package/dist/read-file-tool.d.ts +19 -9
  56. package/dist/read-file-tool.d.ts.map +1 -0
  57. package/dist/read-file-tool.js +27 -50
  58. package/dist/tool-factory-config.cjs +61 -0
  59. package/dist/{tool-provider.d.ts → tool-factory-config.d.ts} +13 -30
  60. package/dist/tool-factory-config.d.ts.map +1 -0
  61. package/dist/tool-factory-config.js +36 -0
  62. package/dist/tool-factory.cjs +123 -0
  63. package/dist/tool-factory.d.ts +4 -0
  64. package/dist/tool-factory.d.ts.map +1 -0
  65. package/dist/tool-factory.js +102 -0
  66. package/dist/types.d.ts +82 -18
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/write-file-tool.cjs +93 -99
  69. package/dist/write-file-tool.d.ts +22 -9
  70. package/dist/write-file-tool.d.ts.map +1 -0
  71. package/dist/write-file-tool.js +97 -91
  72. package/dist/write-file-tool.test.cjs +139 -33
  73. package/dist/write-file-tool.test.d.ts +7 -2
  74. package/dist/write-file-tool.test.d.ts.map +1 -0
  75. package/dist/write-file-tool.test.js +139 -33
  76. package/package.json +5 -4
  77. package/dist/directory-approval.integration.test.d.cts +0 -2
  78. package/dist/edit-file-tool.d.cts +0 -17
  79. package/dist/edit-file-tool.test.d.cts +0 -2
  80. package/dist/error-codes.d.cts +0 -32
  81. package/dist/errors.d.cts +0 -112
  82. package/dist/file-tool-types.d.cts +0 -46
  83. package/dist/filesystem-service.d.cts +0 -112
  84. package/dist/filesystem-service.test.d.cts +0 -2
  85. package/dist/glob-files-tool.d.cts +0 -17
  86. package/dist/grep-content-tool.d.cts +0 -17
  87. package/dist/path-validator.d.cts +0 -97
  88. package/dist/path-validator.test.d.cts +0 -2
  89. package/dist/read-file-tool.d.cts +0 -17
  90. package/dist/tool-provider.cjs +0 -123
  91. package/dist/tool-provider.d.cts +0 -77
  92. package/dist/tool-provider.js +0 -99
  93. package/dist/types.d.cts +0 -178
  94. package/dist/write-file-tool.d.cts +0 -17
  95. package/dist/write-file-tool.test.d.cts +0 -2
@@ -1,18 +1,22 @@
1
- import * as path from "node:path";
2
1
  import { createHash } from "node:crypto";
3
2
  import { z } from "zod";
4
3
  import { createPatch } from "diff";
5
- import { ApprovalType } from "@dexto/core";
6
- import { ToolError } from "@dexto/core";
7
- import { ToolErrorCode } from "@dexto/core";
8
- import { DextoRuntimeError } from "@dexto/core";
4
+ import {
5
+ createLocalToolCallHeader,
6
+ DextoRuntimeError,
7
+ ToolError,
8
+ ToolErrorCode,
9
+ defineTool,
10
+ truncateForHeader
11
+ } from "@dexto/core";
9
12
  import { FileSystemErrorCode } from "./error-codes.js";
13
+ import { createDirectoryAccessApprovalHandlers, resolveFilePath } from "./directory-approval.js";
10
14
  const previewContentHashCache = /* @__PURE__ */ new Map();
11
15
  function computeContentHash(content) {
12
16
  return createHash("sha256").update(content, "utf8").digest("hex");
13
17
  }
14
18
  const EditFileInputSchema = z.object({
15
- file_path: z.string().describe("Absolute path to the file to edit"),
19
+ file_path: z.string().min(1).describe("Absolute path to the file to edit"),
16
20
  old_string: z.string().describe("Text to replace (must be unique unless replace_all is true)"),
17
21
  new_string: z.string().describe("Replacement text"),
18
22
  replace_all: z.boolean().optional().default(false).describe("Replace all occurrences (default: false, requires unique match)")
@@ -25,134 +29,136 @@ function generateDiffPreview(filePath, originalContent, newContent) {
25
29
  const deletions = (unified.match(/^-[^-]/gm) || []).length;
26
30
  return {
27
31
  type: "diff",
32
+ title: "Update file",
28
33
  unified,
29
34
  filename: filePath,
30
35
  additions,
31
36
  deletions
32
37
  };
33
38
  }
34
- function createEditFileTool(options) {
35
- const { fileSystemService, directoryApproval } = options;
36
- let pendingApprovalParentDir;
37
- return {
39
+ function createEditFileTool(getFileSystemService) {
40
+ return defineTool({
38
41
  id: "edit_file",
42
+ aliases: ["edit"],
39
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.",
40
44
  inputSchema: EditFileInputSchema,
41
- /**
42
- * Check if this edit operation needs directory access approval.
43
- * Returns custom approval request if the file is outside allowed paths.
44
- */
45
- getApprovalOverride: async (args) => {
46
- const { file_path } = args;
47
- if (!file_path) return null;
48
- const isAllowed = await fileSystemService.isPathWithinConfigAllowed(file_path);
49
- if (isAllowed) {
50
- return null;
51
- }
52
- if (directoryApproval?.isSessionApproved(file_path)) {
53
- return null;
54
- }
55
- const absolutePath = path.resolve(file_path);
56
- const parentDir = path.dirname(absolutePath);
57
- pendingApprovalParentDir = parentDir;
58
- return {
59
- type: ApprovalType.DIRECTORY_ACCESS,
60
- metadata: {
61
- path: absolutePath,
62
- parentDir,
63
- operation: "edit",
64
- toolName: "edit_file"
65
- }
66
- };
67
- },
68
- /**
69
- * Handle approved directory access - remember the directory for session
70
- */
71
- onApprovalGranted: (response) => {
72
- if (!directoryApproval || !pendingApprovalParentDir) return;
73
- const data = response.data;
74
- const rememberDirectory = data?.rememberDirectory ?? false;
75
- directoryApproval.addApproved(
76
- pendingApprovalParentDir,
77
- rememberDirectory ? "session" : "once"
78
- );
79
- pendingApprovalParentDir = void 0;
80
- },
81
- /**
82
- * Generate preview for approval UI - shows diff without modifying file
83
- * Throws ToolError.validationFailed() for validation errors (file not found, string not found)
84
- * Stores content hash for change detection in execute phase.
85
- */
86
- generatePreview: async (input, context) => {
87
- const { file_path, old_string, new_string, replace_all } = input;
88
- try {
89
- const originalFile = await fileSystemService.readFile(file_path);
90
- const originalContent = originalFile.content;
91
- if (context?.toolCallId) {
92
- previewContentHashCache.set(
93
- context.toolCallId,
94
- computeContentHash(originalContent)
95
- );
96
- }
97
- if (!replace_all) {
98
- const occurrences = originalContent.split(old_string).length - 1;
99
- if (occurrences > 1) {
45
+ ...createDirectoryAccessApprovalHandlers({
46
+ toolName: "edit_file",
47
+ operation: "edit",
48
+ inputSchema: EditFileInputSchema,
49
+ getFileSystemService,
50
+ resolvePaths: (input, fileSystemService) => resolveFilePath(fileSystemService.getWorkingDirectory(), input.file_path)
51
+ }),
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) {
100
102
  throw ToolError.validationFailed(
101
103
  "edit_file",
102
- `String found ${occurrences} times in file. Set replace_all=true to replace all, or provide more context to make old_string unique.`,
103
- { file_path, 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
+ }
104
109
  );
105
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;
106
123
  }
107
- const newContent = replace_all ? originalContent.split(old_string).join(new_string) : originalContent.replace(old_string, new_string);
108
- if (originalContent === newContent) {
109
- throw ToolError.validationFailed(
110
- "edit_file",
111
- `String not found in file: "${old_string.slice(0, 50)}${old_string.length > 50 ? "..." : ""}"`,
112
- { file_path, old_string_preview: old_string.slice(0, 100) }
113
- );
114
- }
115
- return generateDiffPreview(file_path, originalContent, newContent);
116
- } catch (error) {
117
- if (error instanceof DextoRuntimeError && error.code === ToolErrorCode.VALIDATION_FAILED) {
118
- throw error;
119
- }
120
- if (error instanceof DextoRuntimeError) {
121
- throw ToolError.validationFailed("edit_file", error.message, {
122
- file_path,
123
- originalErrorCode: error.code
124
- });
125
- }
126
- return null;
127
124
  }
128
125
  },
129
- execute: async (input, context) => {
126
+ async execute(input, context) {
127
+ const resolvedFileSystemService = await getFileSystemService(context);
130
128
  const { file_path, old_string, new_string, replace_all } = input;
131
- if (context?.toolCallId && previewContentHashCache.has(context.toolCallId)) {
132
- const expectedHash = previewContentHashCache.get(context.toolCallId);
133
- previewContentHashCache.delete(context.toolCallId);
134
- let currentContent;
135
- try {
136
- const currentFile = await fileSystemService.readFile(file_path);
137
- currentContent = currentFile.content;
138
- } catch (error) {
139
- if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.FILE_NOT_FOUND) {
140
- throw ToolError.fileModifiedSincePreview("edit_file", file_path);
129
+ const { path: resolvedPath } = resolveFilePath(
130
+ resolvedFileSystemService.getWorkingDirectory(),
131
+ file_path
132
+ );
133
+ const toolCallId = context.toolCallId;
134
+ if (toolCallId) {
135
+ const expectedHash = previewContentHashCache.get(toolCallId);
136
+ if (expectedHash === void 0) {
137
+ } else {
138
+ previewContentHashCache.delete(toolCallId);
139
+ let currentContent;
140
+ try {
141
+ const currentFile = await resolvedFileSystemService.readFile(resolvedPath);
142
+ currentContent = currentFile.content;
143
+ } catch (error) {
144
+ if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.FILE_NOT_FOUND) {
145
+ throw ToolError.fileModifiedSincePreview("edit_file", resolvedPath);
146
+ }
147
+ throw error;
148
+ }
149
+ const currentHash = computeContentHash(currentContent);
150
+ if (expectedHash !== currentHash) {
151
+ throw ToolError.fileModifiedSincePreview("edit_file", resolvedPath);
141
152
  }
142
- throw error;
143
- }
144
- const currentHash = computeContentHash(currentContent);
145
- if (expectedHash !== currentHash) {
146
- throw ToolError.fileModifiedSincePreview("edit_file", file_path);
147
153
  }
148
154
  }
149
- const result = await fileSystemService.editFile(file_path, {
155
+ const result = await resolvedFileSystemService.editFile(resolvedPath, {
150
156
  oldString: old_string,
151
157
  newString: new_string,
152
158
  replaceAll: replace_all
153
159
  });
154
160
  const _display = generateDiffPreview(
155
- file_path,
161
+ resolvedPath,
156
162
  result.originalContent,
157
163
  result.newContent
158
164
  );
@@ -164,7 +170,7 @@ function createEditFileTool(options) {
164
170
  _display
165
171
  };
166
172
  }
167
- };
173
+ });
168
174
  }
169
175
  export {
170
176
  createEditFileTool
@@ -29,13 +29,26 @@ var import_edit_file_tool = require("./edit-file-tool.js");
29
29
  var import_filesystem_service = require("./filesystem-service.js");
30
30
  var import_core = require("@dexto/core");
31
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
- });
32
+ const createMockLogger = () => {
33
+ const logger = {
34
+ debug: import_vitest.vi.fn(),
35
+ silly: import_vitest.vi.fn(),
36
+ info: import_vitest.vi.fn(),
37
+ warn: import_vitest.vi.fn(),
38
+ error: import_vitest.vi.fn(),
39
+ trackException: import_vitest.vi.fn(),
40
+ createChild: import_vitest.vi.fn(() => logger),
41
+ createFileOnlyChild: import_vitest.vi.fn(() => logger),
42
+ setLevel: import_vitest.vi.fn(),
43
+ getLevel: import_vitest.vi.fn(() => "debug"),
44
+ getLogFilePath: import_vitest.vi.fn(() => null),
45
+ destroy: import_vitest.vi.fn(async () => void 0)
46
+ };
47
+ return logger;
48
+ };
49
+ function createToolContext(logger, overrides = {}) {
50
+ return { logger, ...overrides };
51
+ }
39
52
  (0, import_vitest.describe)("edit_file tool", () => {
40
53
  let mockLogger;
41
54
  let tempDir;
@@ -66,8 +79,43 @@ const createMockLogger = () => ({
66
79
  }
67
80
  });
68
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
+ });
69
115
  (0, import_vitest.it)("should succeed when file is not modified between preview and execute", async () => {
70
- const tool = (0, import_edit_file_tool.createEditFileTool)({ fileSystemService });
116
+ const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
117
+ const previewFn = tool.presentation?.preview;
118
+ (0, import_vitest.expect)(previewFn).toBeDefined();
71
119
  const testFile = path.join(tempDir, "test.txt");
72
120
  await fs.writeFile(testFile, "hello world");
73
121
  const toolCallId = "test-call-123";
@@ -76,16 +124,25 @@ const createMockLogger = () => ({
76
124
  old_string: "world",
77
125
  new_string: "universe"
78
126
  };
79
- const preview = await tool.generatePreview(input, { toolCallId });
127
+ const parsedInput = tool.inputSchema.parse(input);
128
+ const preview = await previewFn(
129
+ parsedInput,
130
+ createToolContext(mockLogger, { toolCallId })
131
+ );
80
132
  (0, import_vitest.expect)(preview).toBeDefined();
81
- const result = await tool.execute(input, { toolCallId });
133
+ const result = await tool.execute(
134
+ parsedInput,
135
+ createToolContext(mockLogger, { toolCallId })
136
+ );
82
137
  (0, import_vitest.expect)(result.success).toBe(true);
83
138
  (0, import_vitest.expect)(result.path).toBe(testFile);
84
139
  const content = await fs.readFile(testFile, "utf-8");
85
140
  (0, import_vitest.expect)(content).toBe("hello universe");
86
141
  });
87
142
  (0, import_vitest.it)("should fail when file is modified between preview and execute", async () => {
88
- const tool = (0, import_edit_file_tool.createEditFileTool)({ fileSystemService });
143
+ const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
144
+ const previewFn = tool.presentation?.preview;
145
+ (0, import_vitest.expect)(previewFn).toBeDefined();
89
146
  const testFile = path.join(tempDir, "test.txt");
90
147
  await fs.writeFile(testFile, "hello world");
91
148
  const toolCallId = "test-call-456";
@@ -94,11 +151,15 @@ const createMockLogger = () => ({
94
151
  old_string: "world",
95
152
  new_string: "universe"
96
153
  };
97
- const preview = await tool.generatePreview(input, { toolCallId });
154
+ const parsedInput = tool.inputSchema.parse(input);
155
+ const preview = await previewFn(
156
+ parsedInput,
157
+ createToolContext(mockLogger, { toolCallId })
158
+ );
98
159
  (0, import_vitest.expect)(preview).toBeDefined();
99
160
  await fs.writeFile(testFile, "hello world - user added this");
100
161
  try {
101
- await tool.execute(input, { toolCallId });
162
+ await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
102
163
  import_vitest.expect.fail("Should have thrown an error");
103
164
  } catch (error) {
104
165
  (0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
@@ -110,7 +171,9 @@ const createMockLogger = () => ({
110
171
  (0, import_vitest.expect)(content).toBe("hello world - user added this");
111
172
  });
112
173
  (0, import_vitest.it)("should detect file modification with correct error code", async () => {
113
- const tool = (0, import_edit_file_tool.createEditFileTool)({ fileSystemService });
174
+ const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
175
+ const previewFn = tool.presentation?.preview;
176
+ (0, import_vitest.expect)(previewFn).toBeDefined();
114
177
  const testFile = path.join(tempDir, "test.txt");
115
178
  await fs.writeFile(testFile, "hello world");
116
179
  const toolCallId = "test-call-789";
@@ -119,10 +182,11 @@ const createMockLogger = () => ({
119
182
  old_string: "world",
120
183
  new_string: "universe"
121
184
  };
122
- await tool.generatePreview(input, { toolCallId });
185
+ const parsedInput = tool.inputSchema.parse(input);
186
+ await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
123
187
  await fs.writeFile(testFile, "hello world modified");
124
188
  try {
125
- await tool.execute(input, { toolCallId });
189
+ await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
126
190
  import_vitest.expect.fail("Should have thrown an error");
127
191
  } catch (error) {
128
192
  (0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
@@ -136,7 +200,9 @@ const createMockLogger = () => ({
136
200
  }
137
201
  });
138
202
  (0, import_vitest.it)("should work without toolCallId (no modification check)", async () => {
139
- const tool = (0, import_edit_file_tool.createEditFileTool)({ fileSystemService });
203
+ const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
204
+ const previewFn = tool.presentation?.preview;
205
+ (0, import_vitest.expect)(previewFn).toBeDefined();
140
206
  const testFile = path.join(tempDir, "test.txt");
141
207
  await fs.writeFile(testFile, "hello world");
142
208
  const input = {
@@ -144,10 +210,11 @@ const createMockLogger = () => ({
144
210
  old_string: "world",
145
211
  new_string: "universe"
146
212
  };
147
- await tool.generatePreview(input, {});
213
+ const parsedInput = tool.inputSchema.parse(input);
214
+ await previewFn(parsedInput, createToolContext(mockLogger));
148
215
  await fs.writeFile(testFile, "hello world changed");
149
216
  try {
150
- await tool.execute(input, {});
217
+ await tool.execute(parsedInput, createToolContext(mockLogger));
151
218
  } catch (error) {
152
219
  (0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
153
220
  (0, import_vitest.expect)(error.code).not.toBe(
@@ -156,7 +223,9 @@ const createMockLogger = () => ({
156
223
  }
157
224
  });
158
225
  (0, import_vitest.it)("should clean up hash cache after successful execution", async () => {
159
- const tool = (0, import_edit_file_tool.createEditFileTool)({ fileSystemService });
226
+ const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
227
+ const previewFn = tool.presentation?.preview;
228
+ (0, import_vitest.expect)(previewFn).toBeDefined();
160
229
  const testFile = path.join(tempDir, "test.txt");
161
230
  await fs.writeFile(testFile, "hello world");
162
231
  const toolCallId = "test-call-cleanup";
@@ -165,21 +234,28 @@ const createMockLogger = () => ({
165
234
  old_string: "world",
166
235
  new_string: "universe"
167
236
  };
168
- await tool.generatePreview(input, { toolCallId });
169
- await tool.execute(input, { toolCallId });
237
+ const parsedInput = tool.inputSchema.parse(input);
238
+ await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
239
+ await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
170
240
  const input2 = {
171
241
  file_path: testFile,
172
242
  old_string: "universe",
173
243
  new_string: "galaxy"
174
244
  };
175
- await tool.generatePreview(input2, { toolCallId });
176
- const result = await tool.execute(input2, { toolCallId });
245
+ const parsedInput2 = tool.inputSchema.parse(input2);
246
+ await previewFn(parsedInput2, createToolContext(mockLogger, { toolCallId }));
247
+ const result = await tool.execute(
248
+ parsedInput2,
249
+ createToolContext(mockLogger, { toolCallId })
250
+ );
177
251
  (0, import_vitest.expect)(result.success).toBe(true);
178
252
  const content = await fs.readFile(testFile, "utf-8");
179
253
  (0, import_vitest.expect)(content).toBe("hello galaxy");
180
254
  });
181
255
  (0, import_vitest.it)("should clean up hash cache after failed execution", async () => {
182
- const tool = (0, import_edit_file_tool.createEditFileTool)({ fileSystemService });
256
+ const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
257
+ const previewFn = tool.presentation?.preview;
258
+ (0, import_vitest.expect)(previewFn).toBeDefined();
183
259
  const testFile = path.join(tempDir, "test.txt");
184
260
  await fs.writeFile(testFile, "hello world");
185
261
  const toolCallId = "test-call-fail-cleanup";
@@ -188,15 +264,19 @@ const createMockLogger = () => ({
188
264
  old_string: "world",
189
265
  new_string: "universe"
190
266
  };
191
- await tool.generatePreview(input, { toolCallId });
267
+ const parsedInput = tool.inputSchema.parse(input);
268
+ await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
192
269
  await fs.writeFile(testFile, "hello world modified");
193
270
  try {
194
- await tool.execute(input, { toolCallId });
271
+ await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
195
272
  } catch {
196
273
  }
197
274
  await fs.writeFile(testFile, "hello world");
198
- await tool.generatePreview(input, { toolCallId });
199
- const result = await tool.execute(input, { toolCallId });
275
+ await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
276
+ const result = await tool.execute(
277
+ parsedInput,
278
+ createToolContext(mockLogger, { toolCallId })
279
+ );
200
280
  (0, import_vitest.expect)(result.success).toBe(true);
201
281
  });
202
282
  });
@@ -1,2 +1,7 @@
1
-
2
- export { }
1
+ /**
2
+ * Edit File Tool Tests
3
+ *
4
+ * Tests for the edit_file tool including file modification detection.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=edit-file-tool.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edit-file-tool.test.d.ts","sourceRoot":"","sources":["../src/edit-file-tool.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}