@dexto/tools-filesystem 1.5.0

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 (62) hide show
  1. package/LICENSE +44 -0
  2. package/dist/directory-approval.integration.test.cjs +467 -0
  3. package/dist/directory-approval.integration.test.d.cts +2 -0
  4. package/dist/directory-approval.integration.test.d.ts +2 -0
  5. package/dist/directory-approval.integration.test.js +444 -0
  6. package/dist/edit-file-tool.cjs +181 -0
  7. package/dist/edit-file-tool.d.cts +17 -0
  8. package/dist/edit-file-tool.d.ts +17 -0
  9. package/dist/edit-file-tool.js +147 -0
  10. package/dist/error-codes.cjs +53 -0
  11. package/dist/error-codes.d.cts +32 -0
  12. package/dist/error-codes.d.ts +32 -0
  13. package/dist/error-codes.js +29 -0
  14. package/dist/errors.cjs +302 -0
  15. package/dist/errors.d.cts +112 -0
  16. package/dist/errors.d.ts +112 -0
  17. package/dist/errors.js +278 -0
  18. package/dist/file-tool-types.cjs +16 -0
  19. package/dist/file-tool-types.d.cts +46 -0
  20. package/dist/file-tool-types.d.ts +46 -0
  21. package/dist/file-tool-types.js +0 -0
  22. package/dist/filesystem-service.cjs +526 -0
  23. package/dist/filesystem-service.d.cts +107 -0
  24. package/dist/filesystem-service.d.ts +107 -0
  25. package/dist/filesystem-service.js +492 -0
  26. package/dist/glob-files-tool.cjs +70 -0
  27. package/dist/glob-files-tool.d.cts +16 -0
  28. package/dist/glob-files-tool.d.ts +16 -0
  29. package/dist/glob-files-tool.js +46 -0
  30. package/dist/grep-content-tool.cjs +86 -0
  31. package/dist/grep-content-tool.d.cts +16 -0
  32. package/dist/grep-content-tool.d.ts +16 -0
  33. package/dist/grep-content-tool.js +62 -0
  34. package/dist/index.cjs +55 -0
  35. package/dist/index.d.cts +14 -0
  36. package/dist/index.d.ts +14 -0
  37. package/dist/index.js +22 -0
  38. package/dist/path-validator.cjs +232 -0
  39. package/dist/path-validator.d.cts +90 -0
  40. package/dist/path-validator.d.ts +90 -0
  41. package/dist/path-validator.js +198 -0
  42. package/dist/path-validator.test.cjs +444 -0
  43. package/dist/path-validator.test.d.cts +2 -0
  44. package/dist/path-validator.test.d.ts +2 -0
  45. package/dist/path-validator.test.js +443 -0
  46. package/dist/read-file-tool.cjs +117 -0
  47. package/dist/read-file-tool.d.cts +17 -0
  48. package/dist/read-file-tool.d.ts +17 -0
  49. package/dist/read-file-tool.js +83 -0
  50. package/dist/tool-provider.cjs +108 -0
  51. package/dist/tool-provider.d.cts +74 -0
  52. package/dist/tool-provider.d.ts +74 -0
  53. package/dist/tool-provider.js +84 -0
  54. package/dist/types.cjs +16 -0
  55. package/dist/types.d.cts +172 -0
  56. package/dist/types.d.ts +172 -0
  57. package/dist/types.js +0 -0
  58. package/dist/write-file-tool.cjs +177 -0
  59. package/dist/write-file-tool.d.cts +17 -0
  60. package/dist/write-file-tool.d.ts +17 -0
  61. package/dist/write-file-tool.js +143 -0
  62. package/package.json +42 -0
@@ -0,0 +1,444 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import * as path from "node:path";
3
+ import * as fs from "node:fs/promises";
4
+ import * as os from "node:os";
5
+ import { createReadFileTool } from "./read-file-tool.js";
6
+ import { createWriteFileTool } from "./write-file-tool.js";
7
+ import { createEditFileTool } from "./edit-file-tool.js";
8
+ import { FileSystemService } from "./filesystem-service.js";
9
+ import { ApprovalType, ApprovalStatus } from "@dexto/core";
10
+ const createMockLogger = () => ({
11
+ debug: vi.fn(),
12
+ info: vi.fn(),
13
+ warn: vi.fn(),
14
+ error: vi.fn(),
15
+ createChild: vi.fn().mockReturnThis()
16
+ });
17
+ describe("Directory Approval Integration Tests", () => {
18
+ let mockLogger;
19
+ let tempDir;
20
+ let fileSystemService;
21
+ let directoryApproval;
22
+ let isSessionApprovedMock;
23
+ let addApprovedMock;
24
+ beforeEach(async () => {
25
+ mockLogger = createMockLogger();
26
+ const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-fs-test-"));
27
+ tempDir = await fs.realpath(rawTempDir);
28
+ fileSystemService = new FileSystemService(
29
+ {
30
+ allowedPaths: [tempDir],
31
+ blockedPaths: [],
32
+ blockedExtensions: [],
33
+ maxFileSize: 10 * 1024 * 1024,
34
+ workingDirectory: tempDir,
35
+ enableBackups: false,
36
+ backupRetentionDays: 7
37
+ },
38
+ mockLogger
39
+ );
40
+ await fileSystemService.initialize();
41
+ isSessionApprovedMock = vi.fn().mockReturnValue(false);
42
+ addApprovedMock = vi.fn();
43
+ directoryApproval = {
44
+ isSessionApproved: isSessionApprovedMock,
45
+ addApproved: addApprovedMock
46
+ };
47
+ vi.clearAllMocks();
48
+ });
49
+ afterEach(async () => {
50
+ try {
51
+ await fs.rm(tempDir, { recursive: true, force: true });
52
+ } catch {
53
+ }
54
+ });
55
+ describe("Read File Tool", () => {
56
+ describe("getApprovalOverride", () => {
57
+ it("should return null for paths within working directory (no prompt needed)", async () => {
58
+ const tool = createReadFileTool({
59
+ fileSystemService,
60
+ directoryApproval
61
+ });
62
+ const testFile = path.join(tempDir, "test.txt");
63
+ await fs.writeFile(testFile, "test content");
64
+ const override = tool.getApprovalOverride?.({ file_path: testFile });
65
+ expect(override).toBeNull();
66
+ });
67
+ it("should return directory access approval for external paths", async () => {
68
+ const tool = createReadFileTool({
69
+ fileSystemService,
70
+ directoryApproval
71
+ });
72
+ const externalPath = "/external/project/file.ts";
73
+ const override = tool.getApprovalOverride?.({ file_path: externalPath });
74
+ expect(override).not.toBeNull();
75
+ expect(override?.type).toBe(ApprovalType.DIRECTORY_ACCESS);
76
+ const metadata = override?.metadata;
77
+ expect(metadata?.path).toBe(path.resolve(externalPath));
78
+ expect(metadata?.parentDir).toBe(path.dirname(path.resolve(externalPath)));
79
+ expect(metadata?.operation).toBe("read");
80
+ expect(metadata?.toolName).toBe("read_file");
81
+ });
82
+ it("should return null when external path is session-approved", async () => {
83
+ isSessionApprovedMock.mockReturnValue(true);
84
+ const tool = createReadFileTool({
85
+ fileSystemService,
86
+ directoryApproval
87
+ });
88
+ const externalPath = "/external/project/file.ts";
89
+ const override = tool.getApprovalOverride?.({ file_path: externalPath });
90
+ expect(override).toBeNull();
91
+ expect(isSessionApprovedMock).toHaveBeenCalledWith(externalPath);
92
+ });
93
+ it("should return null when file_path is missing", async () => {
94
+ const tool = createReadFileTool({
95
+ fileSystemService,
96
+ directoryApproval
97
+ });
98
+ const override = tool.getApprovalOverride?.({});
99
+ expect(override).toBeNull();
100
+ });
101
+ });
102
+ describe("onApprovalGranted", () => {
103
+ it("should add directory as session-approved when rememberDirectory is true", async () => {
104
+ const tool = createReadFileTool({
105
+ fileSystemService,
106
+ directoryApproval
107
+ });
108
+ const externalPath = "/external/project/file.ts";
109
+ tool.getApprovalOverride?.({ file_path: externalPath });
110
+ tool.onApprovalGranted?.({
111
+ approvalId: "test-approval",
112
+ status: ApprovalStatus.APPROVED,
113
+ data: { rememberDirectory: true }
114
+ });
115
+ expect(addApprovedMock).toHaveBeenCalledWith(
116
+ path.dirname(path.resolve(externalPath)),
117
+ "session"
118
+ );
119
+ });
120
+ it("should add directory as once-approved when rememberDirectory is false", async () => {
121
+ const tool = createReadFileTool({
122
+ fileSystemService,
123
+ directoryApproval
124
+ });
125
+ const externalPath = "/external/project/file.ts";
126
+ tool.getApprovalOverride?.({ file_path: externalPath });
127
+ tool.onApprovalGranted?.({
128
+ approvalId: "test-approval",
129
+ status: ApprovalStatus.APPROVED,
130
+ data: { rememberDirectory: false }
131
+ });
132
+ expect(addApprovedMock).toHaveBeenCalledWith(
133
+ path.dirname(path.resolve(externalPath)),
134
+ "once"
135
+ );
136
+ });
137
+ it("should default to once-approved when rememberDirectory is not specified", async () => {
138
+ const tool = createReadFileTool({
139
+ fileSystemService,
140
+ directoryApproval
141
+ });
142
+ const externalPath = "/external/project/file.ts";
143
+ tool.getApprovalOverride?.({ file_path: externalPath });
144
+ tool.onApprovalGranted?.({
145
+ approvalId: "test-approval",
146
+ status: ApprovalStatus.APPROVED,
147
+ data: {}
148
+ });
149
+ expect(addApprovedMock).toHaveBeenCalledWith(
150
+ path.dirname(path.resolve(externalPath)),
151
+ "once"
152
+ );
153
+ });
154
+ it("should not call addApproved when directoryApproval is not provided", async () => {
155
+ const tool = createReadFileTool({
156
+ fileSystemService,
157
+ directoryApproval: void 0
158
+ });
159
+ const externalPath = "/external/project/file.ts";
160
+ tool.getApprovalOverride?.({ file_path: externalPath });
161
+ tool.onApprovalGranted?.({
162
+ approvalId: "test-approval",
163
+ status: ApprovalStatus.APPROVED,
164
+ data: { rememberDirectory: true }
165
+ });
166
+ expect(addApprovedMock).not.toHaveBeenCalled();
167
+ });
168
+ });
169
+ describe("execute", () => {
170
+ it("should read file contents within working directory", async () => {
171
+ const tool = createReadFileTool({
172
+ fileSystemService,
173
+ directoryApproval
174
+ });
175
+ const testFile = path.join(tempDir, "readable.txt");
176
+ await fs.writeFile(testFile, "Hello, world!\nLine 2");
177
+ const result = await tool.execute({ file_path: testFile }, {});
178
+ expect(result.content).toBe("Hello, world!\nLine 2");
179
+ expect(result.lines).toBe(2);
180
+ });
181
+ });
182
+ });
183
+ describe("Write File Tool", () => {
184
+ describe("getApprovalOverride", () => {
185
+ it("should return null for paths within working directory", async () => {
186
+ const tool = createWriteFileTool({
187
+ fileSystemService,
188
+ directoryApproval
189
+ });
190
+ const testFile = path.join(tempDir, "new-file.txt");
191
+ const override = tool.getApprovalOverride?.({
192
+ file_path: testFile,
193
+ content: "test"
194
+ });
195
+ expect(override).toBeNull();
196
+ });
197
+ it("should return directory access approval for external paths", async () => {
198
+ const tool = createWriteFileTool({
199
+ fileSystemService,
200
+ directoryApproval
201
+ });
202
+ const externalPath = "/external/project/new.ts";
203
+ const override = tool.getApprovalOverride?.({
204
+ file_path: externalPath,
205
+ content: "test"
206
+ });
207
+ expect(override).not.toBeNull();
208
+ expect(override?.type).toBe(ApprovalType.DIRECTORY_ACCESS);
209
+ const metadata = override?.metadata;
210
+ expect(metadata?.operation).toBe("write");
211
+ expect(metadata?.toolName).toBe("write_file");
212
+ });
213
+ it("should return null when external path is session-approved", async () => {
214
+ isSessionApprovedMock.mockReturnValue(true);
215
+ const tool = createWriteFileTool({
216
+ fileSystemService,
217
+ directoryApproval
218
+ });
219
+ const externalPath = "/external/project/new.ts";
220
+ const override = tool.getApprovalOverride?.({
221
+ file_path: externalPath,
222
+ content: "test"
223
+ });
224
+ expect(override).toBeNull();
225
+ });
226
+ });
227
+ describe("onApprovalGranted", () => {
228
+ it("should add directory as session-approved when rememberDirectory is true", async () => {
229
+ const tool = createWriteFileTool({
230
+ fileSystemService,
231
+ directoryApproval
232
+ });
233
+ const externalPath = "/external/project/new.ts";
234
+ tool.getApprovalOverride?.({ file_path: externalPath, content: "test" });
235
+ tool.onApprovalGranted?.({
236
+ approvalId: "test-approval",
237
+ status: ApprovalStatus.APPROVED,
238
+ data: { rememberDirectory: true }
239
+ });
240
+ expect(addApprovedMock).toHaveBeenCalledWith(
241
+ path.dirname(path.resolve(externalPath)),
242
+ "session"
243
+ );
244
+ });
245
+ });
246
+ });
247
+ describe("Edit File Tool", () => {
248
+ describe("getApprovalOverride", () => {
249
+ it("should return null for paths within working directory", async () => {
250
+ const tool = createEditFileTool({
251
+ fileSystemService,
252
+ directoryApproval
253
+ });
254
+ const testFile = path.join(tempDir, "existing.txt");
255
+ const override = tool.getApprovalOverride?.({
256
+ file_path: testFile,
257
+ old_string: "old",
258
+ new_string: "new"
259
+ });
260
+ expect(override).toBeNull();
261
+ });
262
+ it("should return directory access approval for external paths", async () => {
263
+ const tool = createEditFileTool({
264
+ fileSystemService,
265
+ directoryApproval
266
+ });
267
+ const externalPath = "/external/project/existing.ts";
268
+ const override = tool.getApprovalOverride?.({
269
+ file_path: externalPath,
270
+ old_string: "old",
271
+ new_string: "new"
272
+ });
273
+ expect(override).not.toBeNull();
274
+ expect(override?.type).toBe(ApprovalType.DIRECTORY_ACCESS);
275
+ const metadata = override?.metadata;
276
+ expect(metadata?.operation).toBe("edit");
277
+ expect(metadata?.toolName).toBe("edit_file");
278
+ });
279
+ it("should return null when external path is session-approved", async () => {
280
+ isSessionApprovedMock.mockReturnValue(true);
281
+ const tool = createEditFileTool({
282
+ fileSystemService,
283
+ directoryApproval
284
+ });
285
+ const externalPath = "/external/project/existing.ts";
286
+ const override = tool.getApprovalOverride?.({
287
+ file_path: externalPath,
288
+ old_string: "old",
289
+ new_string: "new"
290
+ });
291
+ expect(override).toBeNull();
292
+ });
293
+ });
294
+ });
295
+ describe("Session vs Once Approval Scenarios", () => {
296
+ it("should not prompt for subsequent requests after session approval", async () => {
297
+ const tool = createReadFileTool({
298
+ fileSystemService,
299
+ directoryApproval
300
+ });
301
+ const externalPath1 = "/external/project/file1.ts";
302
+ const externalPath2 = "/external/project/file2.ts";
303
+ let override = tool.getApprovalOverride?.({ file_path: externalPath1 });
304
+ expect(override).not.toBeNull();
305
+ tool.onApprovalGranted?.({
306
+ approvalId: "approval-1",
307
+ status: ApprovalStatus.APPROVED,
308
+ data: { rememberDirectory: true }
309
+ });
310
+ expect(addApprovedMock).toHaveBeenCalledWith(
311
+ path.dirname(path.resolve(externalPath1)),
312
+ "session"
313
+ );
314
+ isSessionApprovedMock.mockReturnValue(true);
315
+ override = tool.getApprovalOverride?.({ file_path: externalPath2 });
316
+ expect(override).toBeNull();
317
+ });
318
+ it("should prompt for subsequent requests after once approval", async () => {
319
+ const tool = createReadFileTool({
320
+ fileSystemService,
321
+ directoryApproval
322
+ });
323
+ const externalPath1 = "/external/project/file1.ts";
324
+ const externalPath2 = "/external/project/file2.ts";
325
+ let override = tool.getApprovalOverride?.({ file_path: externalPath1 });
326
+ expect(override).not.toBeNull();
327
+ tool.onApprovalGranted?.({
328
+ approvalId: "approval-1",
329
+ status: ApprovalStatus.APPROVED,
330
+ data: { rememberDirectory: false }
331
+ });
332
+ expect(addApprovedMock).toHaveBeenCalledWith(
333
+ path.dirname(path.resolve(externalPath1)),
334
+ "once"
335
+ );
336
+ isSessionApprovedMock.mockReturnValue(false);
337
+ override = tool.getApprovalOverride?.({ file_path: externalPath2 });
338
+ expect(override).not.toBeNull();
339
+ });
340
+ });
341
+ describe("Path Containment Scenarios", () => {
342
+ it("should cover child paths when parent directory is session-approved", async () => {
343
+ const tool = createReadFileTool({
344
+ fileSystemService,
345
+ directoryApproval
346
+ });
347
+ isSessionApprovedMock.mockImplementation((filePath) => {
348
+ const normalizedPath = path.resolve(filePath);
349
+ const approvedDir = "/external/project";
350
+ return normalizedPath.startsWith(approvedDir + path.sep) || normalizedPath === approvedDir;
351
+ });
352
+ let override = tool.getApprovalOverride?.({ file_path: "/external/project/file.ts" });
353
+ expect(override).toBeNull();
354
+ override = tool.getApprovalOverride?.({
355
+ file_path: "/external/project/deep/nested/file.ts"
356
+ });
357
+ expect(override).toBeNull();
358
+ });
359
+ it("should NOT cover sibling directories", async () => {
360
+ const tool = createReadFileTool({
361
+ fileSystemService,
362
+ directoryApproval
363
+ });
364
+ isSessionApprovedMock.mockImplementation((filePath) => {
365
+ const normalizedPath = path.resolve(filePath);
366
+ const approvedDir = "/external/sub";
367
+ return normalizedPath.startsWith(approvedDir + path.sep) || normalizedPath === approvedDir;
368
+ });
369
+ let override = tool.getApprovalOverride?.({ file_path: "/external/sub/file.ts" });
370
+ expect(override).toBeNull();
371
+ override = tool.getApprovalOverride?.({ file_path: "/external/other/file.ts" });
372
+ expect(override).not.toBeNull();
373
+ });
374
+ });
375
+ describe("Different External Directories Scenarios", () => {
376
+ it("should require separate approval for different external directories", async () => {
377
+ const tool = createReadFileTool({
378
+ fileSystemService,
379
+ directoryApproval
380
+ });
381
+ const dir1Path = "/external/project1/file.ts";
382
+ const dir2Path = "/external/project2/file.ts";
383
+ const override1 = tool.getApprovalOverride?.({ file_path: dir1Path });
384
+ expect(override1).not.toBeNull();
385
+ const metadata1 = override1?.metadata;
386
+ expect(metadata1?.parentDir).toBe("/external/project1");
387
+ const override2 = tool.getApprovalOverride?.({ file_path: dir2Path });
388
+ expect(override2).not.toBeNull();
389
+ const metadata2 = override2?.metadata;
390
+ expect(metadata2?.parentDir).toBe("/external/project2");
391
+ });
392
+ });
393
+ describe("Mixed Operations Scenarios", () => {
394
+ it("should share directory approval across different file operations", async () => {
395
+ const readTool = createReadFileTool({ fileSystemService, directoryApproval });
396
+ const writeTool = createWriteFileTool({ fileSystemService, directoryApproval });
397
+ const editTool = createEditFileTool({ fileSystemService, directoryApproval });
398
+ const externalDir = "/external/project";
399
+ expect(
400
+ readTool.getApprovalOverride?.({ file_path: `${externalDir}/file1.ts` })
401
+ ).not.toBeNull();
402
+ expect(
403
+ writeTool.getApprovalOverride?.({
404
+ file_path: `${externalDir}/file2.ts`,
405
+ content: "test"
406
+ })
407
+ ).not.toBeNull();
408
+ expect(
409
+ editTool.getApprovalOverride?.({
410
+ file_path: `${externalDir}/file3.ts`,
411
+ old_string: "a",
412
+ new_string: "b"
413
+ })
414
+ ).not.toBeNull();
415
+ isSessionApprovedMock.mockReturnValue(true);
416
+ expect(
417
+ readTool.getApprovalOverride?.({ file_path: `${externalDir}/file1.ts` })
418
+ ).toBeNull();
419
+ expect(
420
+ writeTool.getApprovalOverride?.({
421
+ file_path: `${externalDir}/file2.ts`,
422
+ content: "test"
423
+ })
424
+ ).toBeNull();
425
+ expect(
426
+ editTool.getApprovalOverride?.({
427
+ file_path: `${externalDir}/file3.ts`,
428
+ old_string: "a",
429
+ new_string: "b"
430
+ })
431
+ ).toBeNull();
432
+ });
433
+ });
434
+ describe("Without Directory Approval Callbacks", () => {
435
+ it("should work without directory approval callbacks (all paths need normal tool confirmation)", async () => {
436
+ const tool = createReadFileTool({
437
+ fileSystemService,
438
+ directoryApproval: void 0
439
+ });
440
+ const override = tool.getApprovalOverride?.({ file_path: "/external/project/file.ts" });
441
+ expect(override).not.toBeNull();
442
+ });
443
+ });
444
+ });
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var edit_file_tool_exports = {};
30
+ __export(edit_file_tool_exports, {
31
+ createEditFileTool: () => createEditFileTool
32
+ });
33
+ module.exports = __toCommonJS(edit_file_tool_exports);
34
+ var path = __toESM(require("node:path"), 1);
35
+ var import_zod = require("zod");
36
+ var import_diff = require("diff");
37
+ var import_core = require("@dexto/core");
38
+ var import_core2 = require("@dexto/core");
39
+ var import_core3 = require("@dexto/core");
40
+ var import_core4 = require("@dexto/core");
41
+ const EditFileInputSchema = import_zod.z.object({
42
+ file_path: import_zod.z.string().describe("Absolute path to the file to edit"),
43
+ old_string: import_zod.z.string().describe("Text to replace (must be unique unless replace_all is true)"),
44
+ new_string: import_zod.z.string().describe("Replacement text"),
45
+ replace_all: import_zod.z.boolean().optional().default(false).describe("Replace all occurrences (default: false, requires unique match)")
46
+ }).strict();
47
+ function generateDiffPreview(filePath, originalContent, newContent) {
48
+ const unified = (0, import_diff.createPatch)(filePath, originalContent, newContent, "before", "after", {
49
+ context: 3
50
+ });
51
+ const additions = (unified.match(/^\+[^+]/gm) || []).length;
52
+ const deletions = (unified.match(/^-[^-]/gm) || []).length;
53
+ return {
54
+ type: "diff",
55
+ unified,
56
+ filename: filePath,
57
+ additions,
58
+ deletions
59
+ };
60
+ }
61
+ function createEditFileTool(options) {
62
+ const { fileSystemService, directoryApproval } = options;
63
+ let pendingApprovalParentDir;
64
+ return {
65
+ id: "edit_file",
66
+ 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.",
67
+ inputSchema: EditFileInputSchema,
68
+ /**
69
+ * Check if this edit operation needs directory access approval.
70
+ * Returns custom approval request if the file is outside allowed paths.
71
+ */
72
+ getApprovalOverride: (args) => {
73
+ const { file_path } = args;
74
+ if (!file_path) return null;
75
+ const isAllowed = fileSystemService.isPathWithinConfigAllowed(file_path);
76
+ if (isAllowed) {
77
+ return null;
78
+ }
79
+ if (directoryApproval?.isSessionApproved(file_path)) {
80
+ return null;
81
+ }
82
+ const absolutePath = path.resolve(file_path);
83
+ const parentDir = path.dirname(absolutePath);
84
+ pendingApprovalParentDir = parentDir;
85
+ return {
86
+ type: import_core.ApprovalType.DIRECTORY_ACCESS,
87
+ metadata: {
88
+ path: absolutePath,
89
+ parentDir,
90
+ operation: "edit",
91
+ toolName: "edit_file"
92
+ }
93
+ };
94
+ },
95
+ /**
96
+ * Handle approved directory access - remember the directory for session
97
+ */
98
+ onApprovalGranted: (response) => {
99
+ if (!directoryApproval || !pendingApprovalParentDir) return;
100
+ const data = response.data;
101
+ const rememberDirectory = data?.rememberDirectory ?? false;
102
+ directoryApproval.addApproved(
103
+ pendingApprovalParentDir,
104
+ rememberDirectory ? "session" : "once"
105
+ );
106
+ pendingApprovalParentDir = void 0;
107
+ },
108
+ /**
109
+ * Generate preview for approval UI - shows diff without modifying file
110
+ * Throws ToolError.validationFailed() for validation errors (file not found, string not found)
111
+ */
112
+ generatePreview: async (input, _context) => {
113
+ const { file_path, old_string, new_string, replace_all } = input;
114
+ try {
115
+ const originalFile = await fileSystemService.readFile(file_path);
116
+ const originalContent = originalFile.content;
117
+ if (!replace_all) {
118
+ const occurrences = originalContent.split(old_string).length - 1;
119
+ if (occurrences > 1) {
120
+ throw import_core2.ToolError.validationFailed(
121
+ "edit_file",
122
+ `String found ${occurrences} times in file. Set replace_all=true to replace all, or provide more context to make old_string unique.`,
123
+ { file_path, occurrences }
124
+ );
125
+ }
126
+ }
127
+ const newContent = replace_all ? originalContent.split(old_string).join(new_string) : originalContent.replace(old_string, new_string);
128
+ if (originalContent === newContent) {
129
+ throw import_core2.ToolError.validationFailed(
130
+ "edit_file",
131
+ `String not found in file: "${old_string.slice(0, 50)}${old_string.length > 50 ? "..." : ""}"`,
132
+ { file_path, old_string_preview: old_string.slice(0, 100) }
133
+ );
134
+ }
135
+ return generateDiffPreview(file_path, originalContent, newContent);
136
+ } catch (error) {
137
+ if (error instanceof import_core4.DextoRuntimeError && error.code === import_core3.ToolErrorCode.VALIDATION_FAILED) {
138
+ throw error;
139
+ }
140
+ if (error instanceof import_core4.DextoRuntimeError) {
141
+ throw import_core2.ToolError.validationFailed("edit_file", error.message, {
142
+ file_path,
143
+ originalErrorCode: error.code
144
+ });
145
+ }
146
+ return null;
147
+ }
148
+ },
149
+ execute: async (input, _context) => {
150
+ const { file_path, old_string, new_string, replace_all } = input;
151
+ const originalFile = await fileSystemService.readFile(file_path);
152
+ const originalContent = originalFile.content;
153
+ const result = await fileSystemService.editFile(
154
+ file_path,
155
+ {
156
+ oldString: old_string,
157
+ newString: new_string,
158
+ replaceAll: replace_all
159
+ },
160
+ {
161
+ backup: true
162
+ // Always create backup for internal tools
163
+ }
164
+ );
165
+ const newFile = await fileSystemService.readFile(file_path);
166
+ const newContent = newFile.content;
167
+ const _display = generateDiffPreview(file_path, originalContent, newContent);
168
+ return {
169
+ success: result.success,
170
+ path: result.path,
171
+ changes_count: result.changesCount,
172
+ ...result.backupPath && { backup_path: result.backupPath },
173
+ _display
174
+ };
175
+ }
176
+ };
177
+ }
178
+ // Annotate the CommonJS export names for ESM import in node:
179
+ 0 && (module.exports = {
180
+ createEditFileTool
181
+ });
@@ -0,0 +1,17 @@
1
+ import { InternalTool } from '@dexto/core';
2
+ import { FileToolOptions } from './file-tool-types.cjs';
3
+ import './filesystem-service.cjs';
4
+ import './types.cjs';
5
+
6
+ /**
7
+ * Edit File Tool
8
+ *
9
+ * Internal tool for editing files by replacing text (requires approval)
10
+ */
11
+
12
+ /**
13
+ * Create the edit_file internal tool with directory approval support
14
+ */
15
+ declare function createEditFileTool(options: FileToolOptions): InternalTool;
16
+
17
+ export { createEditFileTool };
@@ -0,0 +1,17 @@
1
+ import { InternalTool } from '@dexto/core';
2
+ import { FileToolOptions } from './file-tool-types.js';
3
+ import './filesystem-service.js';
4
+ import './types.js';
5
+
6
+ /**
7
+ * Edit File Tool
8
+ *
9
+ * Internal tool for editing files by replacing text (requires approval)
10
+ */
11
+
12
+ /**
13
+ * Create the edit_file internal tool with directory approval support
14
+ */
15
+ declare function createEditFileTool(options: FileToolOptions): InternalTool;
16
+
17
+ export { createEditFileTool };