@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
@@ -1,7 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { z } from "zod";
3
3
  import { createPatch } from "diff";
4
- import { DextoRuntimeError, ToolError, defineTool } from "@dexto/core";
4
+ import {
5
+ createLocalToolCallHeader,
6
+ DextoRuntimeError,
7
+ ToolError,
8
+ defineTool,
9
+ truncateForHeader
10
+ } from "@dexto/core";
5
11
  import { FileSystemErrorCode } from "./error-codes.js";
6
12
  import { createDirectoryAccessApprovalHandlers, resolveFilePath } from "./directory-approval.js";
7
13
  const previewContentHashCache = /* @__PURE__ */ new Map();
@@ -23,6 +29,7 @@ function generateDiffPreview(filePath, originalContent, newContent) {
23
29
  const deletions = (unified.match(/^-[^-]/gm) || []).length;
24
30
  return {
25
31
  type: "diff",
32
+ title: "Update file",
26
33
  unified,
27
34
  filename: filePath,
28
35
  additions,
@@ -32,55 +39,74 @@ function generateDiffPreview(filePath, originalContent, newContent) {
32
39
  function createWriteFileTool(getFileSystemService) {
33
40
  return defineTool({
34
41
  id: "write_file",
35
- displayName: "Write",
36
42
  aliases: ["write"],
37
43
  description: "Write content to a file. Creates a new file or overwrites existing file. Automatically creates backup of existing files before overwriting. Use create_dirs to create parent directories. Requires approval for all write operations. Returns success status, path, bytes written, and backup path if applicable.",
38
44
  inputSchema: WriteFileInputSchema,
39
45
  ...createDirectoryAccessApprovalHandlers({
40
46
  toolName: "write_file",
41
47
  operation: "write",
48
+ inputSchema: WriteFileInputSchema,
42
49
  getFileSystemService,
43
50
  resolvePaths: (input, fileSystemService) => resolveFilePath(fileSystemService.getWorkingDirectory(), input.file_path)
44
51
  }),
45
- /**
46
- * Generate preview for approval UI - shows diff or file creation info
47
- * Stores content hash for change detection in execute phase.
48
- */
49
- async generatePreview(input, context) {
50
- const { file_path, content } = 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
- return generateDiffPreview(resolvedPath, originalContent, content);
66
- } catch (error) {
67
- if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.FILE_NOT_FOUND) {
52
+ presentation: {
53
+ describeHeader: (input) => createLocalToolCallHeader({
54
+ title: "Write",
55
+ argsText: truncateForHeader(input.file_path, 140)
56
+ }),
57
+ /**
58
+ * Generate preview for approval UI - shows diff or file creation info
59
+ * Stores content hash for change detection in execute phase.
60
+ */
61
+ preview: async (input, context) => {
62
+ const { file_path, content } = input;
63
+ const resolvedFileSystemService = await getFileSystemService(context);
64
+ const { path: resolvedPath } = resolveFilePath(
65
+ resolvedFileSystemService.getWorkingDirectory(),
66
+ file_path
67
+ );
68
+ try {
69
+ let originalContent;
70
+ try {
71
+ const originalFile = await resolvedFileSystemService.readFile(resolvedPath);
72
+ originalContent = originalFile.content;
73
+ } catch (error) {
74
+ if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.INVALID_PATH) {
75
+ const originalFile = await resolvedFileSystemService.readFileForToolPreview(
76
+ resolvedPath
77
+ );
78
+ originalContent = originalFile.content;
79
+ } else {
80
+ throw error;
81
+ }
82
+ }
68
83
  if (context.toolCallId) {
69
- previewContentHashCache.set(context.toolCallId, FILE_NOT_EXISTS_MARKER);
84
+ previewContentHashCache.set(
85
+ context.toolCallId,
86
+ computeContentHash(originalContent)
87
+ );
88
+ }
89
+ return generateDiffPreview(resolvedPath, originalContent, content);
90
+ } catch (error) {
91
+ if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.FILE_NOT_FOUND) {
92
+ if (context.toolCallId) {
93
+ previewContentHashCache.set(context.toolCallId, FILE_NOT_EXISTS_MARKER);
94
+ }
95
+ const lineCount = content.split("\n").length;
96
+ const preview = {
97
+ type: "file",
98
+ title: "Create file",
99
+ path: resolvedPath,
100
+ operation: "create",
101
+ size: Buffer.byteLength(content, "utf8"),
102
+ lineCount,
103
+ content
104
+ // Include content for approval preview
105
+ };
106
+ return preview;
70
107
  }
71
- const lineCount = content.split("\n").length;
72
- const preview = {
73
- type: "file",
74
- path: resolvedPath,
75
- operation: "create",
76
- size: Buffer.byteLength(content, "utf8"),
77
- lineCount,
78
- content
79
- // Include content for approval preview
80
- };
81
- return preview;
108
+ throw error;
82
109
  }
83
- throw error;
84
110
  }
85
111
  },
86
112
  async execute(input, context) {
@@ -136,10 +162,12 @@ function createWriteFileTool(getFileSystemService) {
136
162
  const lineCount = content.split("\n").length;
137
163
  _display = {
138
164
  type: "file",
165
+ title: "Create file",
139
166
  path: resolvedPath,
140
167
  operation: "create",
141
168
  size: result.bytesWritten,
142
- lineCount
169
+ lineCount,
170
+ content
143
171
  };
144
172
  } else {
145
173
  _display = generateDiffPreview(resolvedPath, originalContent, content);
@@ -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,6 +79,36 @@ function createToolContext(logger, overrides = {}) {
78
79
  }
79
80
  });
80
81
  (0, import_vitest.describe)("File Modification Detection - Existing Files", () => {
82
+ (0, import_vitest.it)("should generate preview for existing files outside config-allowed roots (preview read only)", async () => {
83
+ const tool = (0, import_write_file_tool.createWriteFileTool)(async () => fileSystemService);
84
+ const rawExternalDir = await fs.mkdtemp(
85
+ path.join(os.tmpdir(), "dexto-write-outside-allowed-")
86
+ );
87
+ const externalDir = await fs.realpath(rawExternalDir);
88
+ const externalFile = path.join(externalDir, "external.txt");
89
+ try {
90
+ await fs.writeFile(externalFile, "original content");
91
+ const toolCallId = "preview-outside-roots";
92
+ const parsedInput = tool.inputSchema.parse({
93
+ file_path: externalFile,
94
+ content: "new content"
95
+ });
96
+ const preview = await tool.presentation.preview(
97
+ parsedInput,
98
+ createToolContext(mockLogger, { toolCallId })
99
+ );
100
+ (0, import_vitest.expect)(preview).toBeDefined();
101
+ (0, import_vitest.expect)(preview?.type).toBe("diff");
102
+ if (preview?.type === "diff") {
103
+ (0, import_vitest.expect)(preview.title).toBe("Update file");
104
+ (0, import_vitest.expect)(preview.filename).toBe(externalFile);
105
+ } else {
106
+ import_vitest.expect.fail("Expected diff preview");
107
+ }
108
+ } finally {
109
+ await fs.rm(externalDir, { recursive: true, force: true });
110
+ }
111
+ });
81
112
  (0, import_vitest.it)("should succeed when existing file is not modified between preview and execute", async () => {
82
113
  const tool = (0, import_write_file_tool.createWriteFileTool)(async () => fileSystemService);
83
114
  const testFile = path.join(tempDir, "test.txt");
@@ -88,12 +119,17 @@ function createToolContext(logger, overrides = {}) {
88
119
  content: "new content"
89
120
  };
90
121
  const parsedInput = tool.inputSchema.parse(input);
91
- const preview = await tool.generatePreview(
122
+ const preview = await tool.presentation.preview(
92
123
  parsedInput,
93
124
  createToolContext(mockLogger, { toolCallId })
94
125
  );
95
126
  (0, import_vitest.expect)(preview).toBeDefined();
96
127
  (0, import_vitest.expect)(preview?.type).toBe("diff");
128
+ if (preview?.type === "diff") {
129
+ (0, import_vitest.expect)(preview.title).toBe("Update file");
130
+ } else {
131
+ import_vitest.expect.fail("Expected diff preview");
132
+ }
97
133
  const result = await tool.execute(
98
134
  parsedInput,
99
135
  createToolContext(mockLogger, { toolCallId })
@@ -113,7 +149,10 @@ function createToolContext(logger, overrides = {}) {
113
149
  content: "new content"
114
150
  };
115
151
  const parsedInput = tool.inputSchema.parse(input);
116
- await tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
152
+ await tool.presentation.preview(
153
+ parsedInput,
154
+ createToolContext(mockLogger, { toolCallId })
155
+ );
117
156
  await fs.writeFile(testFile, "user modified this");
118
157
  try {
119
158
  await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
@@ -137,7 +176,10 @@ function createToolContext(logger, overrides = {}) {
137
176
  content: "new content"
138
177
  };
139
178
  const parsedInput = tool.inputSchema.parse(input);
140
- await tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
179
+ await tool.presentation.preview(
180
+ parsedInput,
181
+ createToolContext(mockLogger, { toolCallId })
182
+ );
141
183
  await fs.unlink(testFile);
142
184
  try {
143
185
  await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
@@ -160,23 +202,39 @@ function createToolContext(logger, overrides = {}) {
160
202
  content: "brand new content"
161
203
  };
162
204
  const parsedInput = tool.inputSchema.parse(input);
163
- const preview = await tool.generatePreview(
205
+ const preview = await tool.presentation.preview(
164
206
  parsedInput,
165
207
  createToolContext(mockLogger, { toolCallId })
166
208
  );
167
209
  (0, import_vitest.expect)(preview).toBeDefined();
168
210
  (0, import_vitest.expect)(preview?.type).toBe("file");
169
- (0, import_vitest.expect)(preview.operation).toBe("create");
211
+ if (preview?.type === "file") {
212
+ (0, import_vitest.expect)(preview.operation).toBe("create");
213
+ (0, import_vitest.expect)(preview.title).toBe("Create file");
214
+ } else {
215
+ import_vitest.expect.fail("Expected file preview");
216
+ }
170
217
  const result = await tool.execute(
171
218
  parsedInput,
172
219
  createToolContext(mockLogger, { toolCallId })
173
220
  );
174
221
  (0, import_vitest.expect)(result.success).toBe(true);
222
+ const display = result._display;
223
+ if (display && typeof display === "object" && "type" in display) {
224
+ (0, import_vitest.expect)(display.type).toBe("file");
225
+ const fileDisplay = display;
226
+ (0, import_vitest.expect)(fileDisplay.title).toBe("Create file");
227
+ (0, import_vitest.expect)(fileDisplay.content).toBe("brand new content");
228
+ } else {
229
+ import_vitest.expect.fail("Expected result._display");
230
+ }
175
231
  const content = await fs.readFile(testFile, "utf-8");
176
232
  (0, import_vitest.expect)(content).toBe("brand new content");
177
233
  });
178
234
  (0, import_vitest.it)("should fail when file is created by someone else between preview and execute", async () => {
179
235
  const tool = (0, import_write_file_tool.createWriteFileTool)(async () => fileSystemService);
236
+ const previewFn = tool.presentation?.preview;
237
+ (0, import_vitest.expect)(previewFn).toBeDefined();
180
238
  const testFile = path.join(tempDir, "race-condition.txt");
181
239
  const toolCallId = "test-call-race";
182
240
  const input = {
@@ -184,7 +242,7 @@ function createToolContext(logger, overrides = {}) {
184
242
  content: "agent content"
185
243
  };
186
244
  const parsedInput = tool.inputSchema.parse(input);
187
- const preview = await tool.generatePreview(
245
+ const preview = await previewFn(
188
246
  parsedInput,
189
247
  createToolContext(mockLogger, { toolCallId })
190
248
  );
@@ -214,17 +272,19 @@ function createToolContext(logger, overrides = {}) {
214
272
  content: "first write"
215
273
  };
216
274
  const parsedInput = tool.inputSchema.parse(input);
217
- await tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
275
+ await tool.presentation.preview(
276
+ parsedInput,
277
+ createToolContext(mockLogger, { toolCallId })
278
+ );
218
279
  await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
219
280
  const input2 = {
220
281
  file_path: testFile,
221
282
  content: "second write"
222
283
  };
223
284
  const parsedInput2 = tool.inputSchema.parse(input2);
224
- await tool.generatePreview(
225
- parsedInput2,
226
- createToolContext(mockLogger, { toolCallId })
227
- );
285
+ const previewFn2 = tool.presentation?.preview;
286
+ (0, import_vitest.expect)(previewFn2).toBeDefined();
287
+ await previewFn2(parsedInput2, createToolContext(mockLogger, { toolCallId }));
228
288
  const result = await tool.execute(
229
289
  parsedInput2,
230
290
  createToolContext(mockLogger, { toolCallId })
@@ -243,14 +303,16 @@ function createToolContext(logger, overrides = {}) {
243
303
  content: "new content"
244
304
  };
245
305
  const parsedInput = tool.inputSchema.parse(input);
246
- await tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
306
+ const previewFn = tool.presentation?.preview;
307
+ (0, import_vitest.expect)(previewFn).toBeDefined();
308
+ await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
247
309
  await fs.writeFile(testFile, "modified");
248
310
  try {
249
311
  await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
250
312
  } catch {
251
313
  }
252
314
  await fs.writeFile(testFile, "reset content");
253
- await tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
315
+ await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
254
316
  const result = await tool.execute(
255
317
  parsedInput,
256
318
  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,6 +56,36 @@ describe("write_file tool", () => {
55
56
  }
56
57
  });
57
58
  describe("File Modification Detection - Existing Files", () => {
59
+ it("should generate preview for existing files outside config-allowed roots (preview read only)", async () => {
60
+ const tool = createWriteFileTool(async () => fileSystemService);
61
+ const rawExternalDir = await fs.mkdtemp(
62
+ path.join(os.tmpdir(), "dexto-write-outside-allowed-")
63
+ );
64
+ const externalDir = await fs.realpath(rawExternalDir);
65
+ const externalFile = path.join(externalDir, "external.txt");
66
+ try {
67
+ await fs.writeFile(externalFile, "original content");
68
+ const toolCallId = "preview-outside-roots";
69
+ const parsedInput = tool.inputSchema.parse({
70
+ file_path: externalFile,
71
+ content: "new content"
72
+ });
73
+ const preview = await tool.presentation.preview(
74
+ parsedInput,
75
+ createToolContext(mockLogger, { toolCallId })
76
+ );
77
+ expect(preview).toBeDefined();
78
+ expect(preview?.type).toBe("diff");
79
+ if (preview?.type === "diff") {
80
+ expect(preview.title).toBe("Update file");
81
+ expect(preview.filename).toBe(externalFile);
82
+ } else {
83
+ expect.fail("Expected diff preview");
84
+ }
85
+ } finally {
86
+ await fs.rm(externalDir, { recursive: true, force: true });
87
+ }
88
+ });
58
89
  it("should succeed when existing file is not modified between preview and execute", async () => {
59
90
  const tool = createWriteFileTool(async () => fileSystemService);
60
91
  const testFile = path.join(tempDir, "test.txt");
@@ -65,12 +96,17 @@ describe("write_file tool", () => {
65
96
  content: "new content"
66
97
  };
67
98
  const parsedInput = tool.inputSchema.parse(input);
68
- const preview = await tool.generatePreview(
99
+ const preview = await tool.presentation.preview(
69
100
  parsedInput,
70
101
  createToolContext(mockLogger, { toolCallId })
71
102
  );
72
103
  expect(preview).toBeDefined();
73
104
  expect(preview?.type).toBe("diff");
105
+ if (preview?.type === "diff") {
106
+ expect(preview.title).toBe("Update file");
107
+ } else {
108
+ expect.fail("Expected diff preview");
109
+ }
74
110
  const result = await tool.execute(
75
111
  parsedInput,
76
112
  createToolContext(mockLogger, { toolCallId })
@@ -90,7 +126,10 @@ describe("write_file tool", () => {
90
126
  content: "new content"
91
127
  };
92
128
  const parsedInput = tool.inputSchema.parse(input);
93
- await tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
129
+ await tool.presentation.preview(
130
+ parsedInput,
131
+ createToolContext(mockLogger, { toolCallId })
132
+ );
94
133
  await fs.writeFile(testFile, "user modified this");
95
134
  try {
96
135
  await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
@@ -114,7 +153,10 @@ describe("write_file tool", () => {
114
153
  content: "new content"
115
154
  };
116
155
  const parsedInput = tool.inputSchema.parse(input);
117
- await tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
156
+ await tool.presentation.preview(
157
+ parsedInput,
158
+ createToolContext(mockLogger, { toolCallId })
159
+ );
118
160
  await fs.unlink(testFile);
119
161
  try {
120
162
  await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
@@ -137,23 +179,39 @@ describe("write_file tool", () => {
137
179
  content: "brand new content"
138
180
  };
139
181
  const parsedInput = tool.inputSchema.parse(input);
140
- const preview = await tool.generatePreview(
182
+ const preview = await tool.presentation.preview(
141
183
  parsedInput,
142
184
  createToolContext(mockLogger, { toolCallId })
143
185
  );
144
186
  expect(preview).toBeDefined();
145
187
  expect(preview?.type).toBe("file");
146
- expect(preview.operation).toBe("create");
188
+ if (preview?.type === "file") {
189
+ expect(preview.operation).toBe("create");
190
+ expect(preview.title).toBe("Create file");
191
+ } else {
192
+ expect.fail("Expected file preview");
193
+ }
147
194
  const result = await tool.execute(
148
195
  parsedInput,
149
196
  createToolContext(mockLogger, { toolCallId })
150
197
  );
151
198
  expect(result.success).toBe(true);
199
+ const display = result._display;
200
+ if (display && typeof display === "object" && "type" in display) {
201
+ expect(display.type).toBe("file");
202
+ const fileDisplay = display;
203
+ expect(fileDisplay.title).toBe("Create file");
204
+ expect(fileDisplay.content).toBe("brand new content");
205
+ } else {
206
+ expect.fail("Expected result._display");
207
+ }
152
208
  const content = await fs.readFile(testFile, "utf-8");
153
209
  expect(content).toBe("brand new content");
154
210
  });
155
211
  it("should fail when file is created by someone else between preview and execute", async () => {
156
212
  const tool = createWriteFileTool(async () => fileSystemService);
213
+ const previewFn = tool.presentation?.preview;
214
+ expect(previewFn).toBeDefined();
157
215
  const testFile = path.join(tempDir, "race-condition.txt");
158
216
  const toolCallId = "test-call-race";
159
217
  const input = {
@@ -161,7 +219,7 @@ describe("write_file tool", () => {
161
219
  content: "agent content"
162
220
  };
163
221
  const parsedInput = tool.inputSchema.parse(input);
164
- const preview = await tool.generatePreview(
222
+ const preview = await previewFn(
165
223
  parsedInput,
166
224
  createToolContext(mockLogger, { toolCallId })
167
225
  );
@@ -191,17 +249,19 @@ describe("write_file tool", () => {
191
249
  content: "first write"
192
250
  };
193
251
  const parsedInput = tool.inputSchema.parse(input);
194
- await tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
252
+ await tool.presentation.preview(
253
+ parsedInput,
254
+ createToolContext(mockLogger, { toolCallId })
255
+ );
195
256
  await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
196
257
  const input2 = {
197
258
  file_path: testFile,
198
259
  content: "second write"
199
260
  };
200
261
  const parsedInput2 = tool.inputSchema.parse(input2);
201
- await tool.generatePreview(
202
- parsedInput2,
203
- createToolContext(mockLogger, { toolCallId })
204
- );
262
+ const previewFn2 = tool.presentation?.preview;
263
+ expect(previewFn2).toBeDefined();
264
+ await previewFn2(parsedInput2, createToolContext(mockLogger, { toolCallId }));
205
265
  const result = await tool.execute(
206
266
  parsedInput2,
207
267
  createToolContext(mockLogger, { toolCallId })
@@ -220,14 +280,16 @@ describe("write_file tool", () => {
220
280
  content: "new content"
221
281
  };
222
282
  const parsedInput = tool.inputSchema.parse(input);
223
- await tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
283
+ const previewFn = tool.presentation?.preview;
284
+ expect(previewFn).toBeDefined();
285
+ await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
224
286
  await fs.writeFile(testFile, "modified");
225
287
  try {
226
288
  await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
227
289
  } catch {
228
290
  }
229
291
  await fs.writeFile(testFile, "reset content");
230
- await tool.generatePreview(parsedInput, createToolContext(mockLogger, { toolCallId }));
292
+ await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
231
293
  const result = await tool.execute(
232
294
  parsedInput,
233
295
  createToolContext(mockLogger, { toolCallId })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dexto/tools-filesystem",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "FileSystem tools factory for Dexto agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,12 +18,12 @@
18
18
  "file-operations"
19
19
  ],
20
20
  "dependencies": {
21
- "diff": "^7.0.0",
22
- "glob": "^11.1.0",
21
+ "diff": "^8.0.3",
22
+ "glob": "^12.0.0",
23
23
  "safe-regex": "^2.1.1",
24
24
  "zod": "^3.25.0",
25
- "@dexto/agent-config": "1.6.0",
26
- "@dexto/core": "1.6.0"
25
+ "@dexto/agent-config": "1.6.2",
26
+ "@dexto/core": "1.6.2"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/diff": "^5.2.3",
@@ -36,7 +36,7 @@
36
36
  "README.md"
37
37
  ],
38
38
  "scripts": {
39
- "build": "tsup",
39
+ "build": "tsup && node ../../scripts/clean-tsbuildinfo.mjs && tsc -b tsconfig.json --emitDeclarationOnly",
40
40
  "typecheck": "tsc --noEmit",
41
41
  "clean": "rm -rf dist"
42
42
  }
@@ -1,22 +0,0 @@
1
- import { ToolExecutionContext, ApprovalRequestDetails, ApprovalResponse } from '@dexto/core';
2
- import { FileSystemService } from './filesystem-service.cjs';
3
- import { FileSystemServiceGetter } from './file-tool-types.cjs';
4
- import './types.cjs';
5
-
6
- type DirectoryApprovalOperation = 'read' | 'write' | 'edit';
7
- type DirectoryApprovalPaths = {
8
- path: string;
9
- parentDir: string;
10
- };
11
- declare function resolveFilePath(workingDirectory: string, filePath: string): DirectoryApprovalPaths;
12
- declare function createDirectoryAccessApprovalHandlers<TInput>(options: {
13
- toolName: string;
14
- operation: DirectoryApprovalOperation;
15
- getFileSystemService: FileSystemServiceGetter;
16
- resolvePaths: (input: TInput, fileSystemService: FileSystemService) => DirectoryApprovalPaths;
17
- }): {
18
- getApprovalOverride: (input: TInput, context: ToolExecutionContext) => Promise<ApprovalRequestDetails | null>;
19
- onApprovalGranted: (response: ApprovalResponse, context: ToolExecutionContext, approvalRequest: ApprovalRequestDetails) => void;
20
- };
21
-
22
- export { createDirectoryAccessApprovalHandlers, resolveFilePath };
@@ -1,2 +0,0 @@
1
-
2
- export { }
@@ -1,34 +0,0 @@
1
- import { z } from 'zod';
2
- import { Tool } from '@dexto/core';
3
- import { FileSystemServiceGetter } from './file-tool-types.cjs';
4
- import './filesystem-service.cjs';
5
- import './types.cjs';
6
-
7
- /**
8
- * Edit File Tool
9
- *
10
- * Internal tool for editing files by replacing text (requires approval)
11
- */
12
-
13
- declare const EditFileInputSchema: z.ZodObject<{
14
- file_path: z.ZodString;
15
- old_string: z.ZodString;
16
- new_string: z.ZodString;
17
- replace_all: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
18
- }, "strict", z.ZodTypeAny, {
19
- file_path: string;
20
- old_string: string;
21
- new_string: string;
22
- replace_all: boolean;
23
- }, {
24
- file_path: string;
25
- old_string: string;
26
- new_string: string;
27
- replace_all?: boolean | undefined;
28
- }>;
29
- /**
30
- * Create the edit_file internal tool with directory approval support
31
- */
32
- declare function createEditFileTool(getFileSystemService: FileSystemServiceGetter): Tool<typeof EditFileInputSchema>;
33
-
34
- export { createEditFileTool };
@@ -1,2 +0,0 @@
1
-
2
- export { }
@@ -1,32 +0,0 @@
1
- /**
2
- * FileSystem Service Error Codes
3
- *
4
- * Standardized error codes for file system operations
5
- */
6
- declare enum FileSystemErrorCode {
7
- FILE_NOT_FOUND = "FILESYSTEM_FILE_NOT_FOUND",
8
- DIRECTORY_NOT_FOUND = "FILESYSTEM_DIRECTORY_NOT_FOUND",
9
- PERMISSION_DENIED = "FILESYSTEM_PERMISSION_DENIED",
10
- PATH_NOT_ALLOWED = "FILESYSTEM_PATH_NOT_ALLOWED",
11
- PATH_BLOCKED = "FILESYSTEM_PATH_BLOCKED",
12
- INVALID_PATH = "FILESYSTEM_INVALID_PATH",
13
- PATH_TRAVERSAL_DETECTED = "FILESYSTEM_PATH_TRAVERSAL_DETECTED",
14
- INVALID_FILE_EXTENSION = "FILESYSTEM_INVALID_FILE_EXTENSION",
15
- INVALID_ENCODING = "FILESYSTEM_INVALID_ENCODING",
16
- FILE_TOO_LARGE = "FILESYSTEM_FILE_TOO_LARGE",
17
- TOO_MANY_RESULTS = "FILESYSTEM_TOO_MANY_RESULTS",
18
- READ_FAILED = "FILESYSTEM_READ_FAILED",
19
- WRITE_FAILED = "FILESYSTEM_WRITE_FAILED",
20
- BACKUP_FAILED = "FILESYSTEM_BACKUP_FAILED",
21
- EDIT_FAILED = "FILESYSTEM_EDIT_FAILED",
22
- STRING_NOT_UNIQUE = "FILESYSTEM_STRING_NOT_UNIQUE",
23
- STRING_NOT_FOUND = "FILESYSTEM_STRING_NOT_FOUND",
24
- GLOB_FAILED = "FILESYSTEM_GLOB_FAILED",
25
- SEARCH_FAILED = "FILESYSTEM_SEARCH_FAILED",
26
- INVALID_PATTERN = "FILESYSTEM_INVALID_PATTERN",
27
- REGEX_TIMEOUT = "FILESYSTEM_REGEX_TIMEOUT",
28
- INVALID_CONFIG = "FILESYSTEM_INVALID_CONFIG",
29
- SERVICE_NOT_INITIALIZED = "FILESYSTEM_SERVICE_NOT_INITIALIZED"
30
- }
31
-
32
- export { FileSystemErrorCode };