@dexto/tools-filesystem 1.5.7 → 1.6.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 (83) hide show
  1. package/dist/directory-approval.cjs +94 -0
  2. package/dist/directory-approval.d.cts +22 -0
  3. package/dist/directory-approval.d.ts +20 -0
  4. package/dist/directory-approval.d.ts.map +1 -0
  5. package/dist/directory-approval.integration.test.cjs +303 -269
  6. package/dist/directory-approval.integration.test.d.ts +14 -2
  7. package/dist/directory-approval.integration.test.d.ts.map +1 -0
  8. package/dist/directory-approval.integration.test.js +309 -270
  9. package/dist/directory-approval.js +59 -0
  10. package/dist/edit-file-tool.cjs +57 -90
  11. package/dist/edit-file-tool.d.cts +20 -3
  12. package/dist/edit-file-tool.d.ts +22 -9
  13. package/dist/edit-file-tool.d.ts.map +1 -0
  14. package/dist/edit-file-tool.js +53 -76
  15. package/dist/edit-file-tool.test.cjs +66 -29
  16. package/dist/edit-file-tool.test.d.ts +7 -2
  17. package/dist/edit-file-tool.test.d.ts.map +1 -0
  18. package/dist/edit-file-tool.test.js +66 -29
  19. package/dist/error-codes.d.ts +2 -3
  20. package/dist/error-codes.d.ts.map +1 -0
  21. package/dist/errors.d.ts +4 -7
  22. package/dist/errors.d.ts.map +1 -0
  23. package/dist/file-tool-types.d.cts +7 -35
  24. package/dist/file-tool-types.d.ts +8 -40
  25. package/dist/file-tool-types.d.ts.map +1 -0
  26. package/dist/filesystem-service.cjs +18 -1
  27. package/dist/filesystem-service.d.cts +11 -6
  28. package/dist/filesystem-service.d.ts +14 -12
  29. package/dist/filesystem-service.d.ts.map +1 -0
  30. package/dist/filesystem-service.js +18 -1
  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 +22 -47
  36. package/dist/glob-files-tool.d.cts +17 -3
  37. package/dist/glob-files-tool.d.ts +19 -9
  38. package/dist/glob-files-tool.d.ts.map +1 -0
  39. package/dist/glob-files-tool.js +23 -48
  40. package/dist/grep-content-tool.cjs +29 -46
  41. package/dist/grep-content-tool.d.cts +26 -3
  42. package/dist/grep-content-tool.d.ts +28 -9
  43. package/dist/grep-content-tool.d.ts.map +1 -0
  44. package/dist/grep-content-tool.js +30 -47
  45. package/dist/index.cjs +3 -3
  46. package/dist/index.d.cts +4 -2
  47. package/dist/index.d.ts +10 -5
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +2 -2
  50. package/dist/path-validator.d.cts +2 -2
  51. package/dist/path-validator.d.ts +6 -9
  52. package/dist/path-validator.d.ts.map +1 -0
  53. package/dist/path-validator.test.d.ts +7 -2
  54. package/dist/path-validator.test.d.ts.map +1 -0
  55. package/dist/read-file-tool.cjs +21 -60
  56. package/dist/read-file-tool.d.cts +17 -3
  57. package/dist/read-file-tool.d.ts +19 -9
  58. package/dist/read-file-tool.d.ts.map +1 -0
  59. package/dist/read-file-tool.js +22 -51
  60. package/dist/tool-factory-config.cjs +61 -0
  61. package/dist/{tool-provider.d.ts → tool-factory-config.d.cts} +9 -23
  62. package/dist/{tool-provider.d.cts → tool-factory-config.d.ts} +13 -30
  63. package/dist/tool-factory-config.d.ts.map +1 -0
  64. package/dist/tool-factory-config.js +36 -0
  65. package/dist/tool-factory.cjs +102 -0
  66. package/dist/tool-factory.d.cts +7 -0
  67. package/dist/tool-factory.d.ts +4 -0
  68. package/dist/tool-factory.d.ts.map +1 -0
  69. package/dist/tool-factory.js +81 -0
  70. package/dist/types.d.ts +17 -18
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/write-file-tool.cjs +45 -73
  73. package/dist/write-file-tool.d.cts +20 -3
  74. package/dist/write-file-tool.d.ts +22 -9
  75. package/dist/write-file-tool.d.ts.map +1 -0
  76. package/dist/write-file-tool.js +46 -68
  77. package/dist/write-file-tool.test.cjs +76 -32
  78. package/dist/write-file-tool.test.d.ts +7 -2
  79. package/dist/write-file-tool.test.d.ts.map +1 -0
  80. package/dist/write-file-tool.test.js +76 -32
  81. package/package.json +4 -3
  82. package/dist/tool-provider.cjs +0 -123
  83. package/dist/tool-provider.js +0 -99
@@ -0,0 +1,59 @@
1
+ import * as path from "node:path";
2
+ import { ApprovalType, ToolError } from "@dexto/core";
3
+ function resolveFilePath(workingDirectory, filePath) {
4
+ const resolvedPath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(workingDirectory, filePath);
5
+ return { path: resolvedPath, parentDir: path.dirname(resolvedPath) };
6
+ }
7
+ function createDirectoryAccessApprovalHandlers(options) {
8
+ return {
9
+ async getApprovalOverride(input, context) {
10
+ const resolvedFileSystemService = await options.getFileSystemService(context);
11
+ const paths = options.resolvePaths(input, resolvedFileSystemService);
12
+ const isAllowed = await resolvedFileSystemService.isPathWithinConfigAllowed(paths.path);
13
+ if (isAllowed) {
14
+ return null;
15
+ }
16
+ const approvalManager = context.services?.approval;
17
+ if (!approvalManager) {
18
+ throw ToolError.configInvalid(
19
+ `${options.toolName} requires ToolExecutionContext.services.approval`
20
+ );
21
+ }
22
+ if (approvalManager.isDirectorySessionApproved(paths.path)) {
23
+ return null;
24
+ }
25
+ return {
26
+ type: ApprovalType.DIRECTORY_ACCESS,
27
+ metadata: {
28
+ path: paths.path,
29
+ parentDir: paths.parentDir,
30
+ operation: options.operation,
31
+ toolName: options.toolName
32
+ }
33
+ };
34
+ },
35
+ onApprovalGranted(response, context, approvalRequest) {
36
+ if (approvalRequest.type !== ApprovalType.DIRECTORY_ACCESS) {
37
+ return;
38
+ }
39
+ const metadata = approvalRequest.metadata;
40
+ const parentDir = typeof metadata?.parentDir === "string" ? metadata.parentDir : null;
41
+ if (!parentDir) {
42
+ return;
43
+ }
44
+ const data = response.data;
45
+ const rememberDirectory = data?.rememberDirectory ?? false;
46
+ const approvalManager = context.services?.approval;
47
+ if (!approvalManager) {
48
+ throw ToolError.configInvalid(
49
+ `${options.toolName} requires ToolExecutionContext.services.approval`
50
+ );
51
+ }
52
+ approvalManager.addApprovedDirectory(parentDir, rememberDirectory ? "session" : "once");
53
+ }
54
+ };
55
+ }
56
+ export {
57
+ createDirectoryAccessApprovalHandlers,
58
+ resolveFilePath
59
+ };
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,35 +15,24 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
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
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
  var edit_file_tool_exports = {};
30
20
  __export(edit_file_tool_exports, {
31
21
  createEditFileTool: () => createEditFileTool
32
22
  });
33
23
  module.exports = __toCommonJS(edit_file_tool_exports);
34
- var path = __toESM(require("node:path"), 1);
35
24
  var import_node_crypto = require("node:crypto");
36
25
  var import_zod = require("zod");
37
26
  var import_diff = require("diff");
38
27
  var import_core = require("@dexto/core");
39
- var import_core2 = require("@dexto/core");
40
- var import_core3 = require("@dexto/core");
41
- var import_core4 = require("@dexto/core");
42
28
  var import_error_codes = require("./error-codes.js");
29
+ var import_directory_approval = require("./directory-approval.js");
43
30
  const previewContentHashCache = /* @__PURE__ */ new Map();
44
31
  function computeContentHash(content) {
45
32
  return (0, import_node_crypto.createHash)("sha256").update(content, "utf8").digest("hex");
46
33
  }
47
34
  const EditFileInputSchema = import_zod.z.object({
48
- file_path: import_zod.z.string().describe("Absolute path to the file to edit"),
35
+ file_path: import_zod.z.string().min(1).describe("Absolute path to the file to edit"),
49
36
  old_string: import_zod.z.string().describe("Text to replace (must be unique unless replace_all is true)"),
50
37
  new_string: import_zod.z.string().describe("Replacement text"),
51
38
  replace_all: import_zod.z.boolean().optional().default(false).describe("Replace all occurrences (default: false, requires unique match)")
@@ -64,64 +51,35 @@ function generateDiffPreview(filePath, originalContent, newContent) {
64
51
  deletions
65
52
  };
66
53
  }
67
- function createEditFileTool(options) {
68
- const { fileSystemService, directoryApproval } = options;
69
- let pendingApprovalParentDir;
70
- return {
54
+ function createEditFileTool(getFileSystemService) {
55
+ return (0, import_core.defineTool)({
71
56
  id: "edit_file",
57
+ displayName: "Update",
58
+ aliases: ["edit"],
72
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.",
73
60
  inputSchema: EditFileInputSchema,
74
- /**
75
- * Check if this edit operation needs directory access approval.
76
- * Returns custom approval request if the file is outside allowed paths.
77
- */
78
- getApprovalOverride: async (args) => {
79
- const { file_path } = args;
80
- if (!file_path) return null;
81
- const isAllowed = await fileSystemService.isPathWithinConfigAllowed(file_path);
82
- if (isAllowed) {
83
- return null;
84
- }
85
- if (directoryApproval?.isSessionApproved(file_path)) {
86
- return null;
87
- }
88
- const absolutePath = path.resolve(file_path);
89
- const parentDir = path.dirname(absolutePath);
90
- pendingApprovalParentDir = parentDir;
91
- return {
92
- type: import_core.ApprovalType.DIRECTORY_ACCESS,
93
- metadata: {
94
- path: absolutePath,
95
- parentDir,
96
- operation: "edit",
97
- toolName: "edit_file"
98
- }
99
- };
100
- },
101
- /**
102
- * Handle approved directory access - remember the directory for session
103
- */
104
- onApprovalGranted: (response) => {
105
- if (!directoryApproval || !pendingApprovalParentDir) return;
106
- const data = response.data;
107
- const rememberDirectory = data?.rememberDirectory ?? false;
108
- directoryApproval.addApproved(
109
- pendingApprovalParentDir,
110
- rememberDirectory ? "session" : "once"
111
- );
112
- pendingApprovalParentDir = void 0;
113
- },
61
+ ...(0, import_directory_approval.createDirectoryAccessApprovalHandlers)({
62
+ toolName: "edit_file",
63
+ operation: "edit",
64
+ getFileSystemService,
65
+ resolvePaths: (input, fileSystemService) => (0, import_directory_approval.resolveFilePath)(fileSystemService.getWorkingDirectory(), input.file_path)
66
+ }),
114
67
  /**
115
68
  * Generate preview for approval UI - shows diff without modifying file
116
69
  * Throws ToolError.validationFailed() for validation errors (file not found, string not found)
117
70
  * Stores content hash for change detection in execute phase.
118
71
  */
119
- generatePreview: async (input, context) => {
72
+ async generatePreview(input, context) {
120
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
+ );
121
79
  try {
122
- const originalFile = await fileSystemService.readFile(file_path);
80
+ const originalFile = await resolvedFileSystemService.readFile(resolvedPath);
123
81
  const originalContent = originalFile.content;
124
- if (context?.toolCallId) {
82
+ if (context.toolCallId) {
125
83
  previewContentHashCache.set(
126
84
  context.toolCallId,
127
85
  computeContentHash(originalContent)
@@ -130,62 +88,71 @@ function createEditFileTool(options) {
130
88
  if (!replace_all) {
131
89
  const occurrences = originalContent.split(old_string).length - 1;
132
90
  if (occurrences > 1) {
133
- throw import_core2.ToolError.validationFailed(
91
+ throw import_core.ToolError.validationFailed(
134
92
  "edit_file",
135
93
  `String found ${occurrences} times in file. Set replace_all=true to replace all, or provide more context to make old_string unique.`,
136
- { file_path, occurrences }
94
+ { file_path: resolvedPath, occurrences }
137
95
  );
138
96
  }
139
97
  }
140
98
  const newContent = replace_all ? originalContent.split(old_string).join(new_string) : originalContent.replace(old_string, new_string);
141
99
  if (originalContent === newContent) {
142
- throw import_core2.ToolError.validationFailed(
100
+ throw import_core.ToolError.validationFailed(
143
101
  "edit_file",
144
102
  `String not found in file: "${old_string.slice(0, 50)}${old_string.length > 50 ? "..." : ""}"`,
145
- { file_path, old_string_preview: old_string.slice(0, 100) }
103
+ { file_path: resolvedPath, old_string_preview: old_string.slice(0, 100) }
146
104
  );
147
105
  }
148
- return generateDiffPreview(file_path, originalContent, newContent);
106
+ return generateDiffPreview(resolvedPath, originalContent, newContent);
149
107
  } catch (error) {
150
- if (error instanceof import_core4.DextoRuntimeError && error.code === import_core3.ToolErrorCode.VALIDATION_FAILED) {
108
+ if (error instanceof import_core.DextoRuntimeError && error.code === import_core.ToolErrorCode.VALIDATION_FAILED) {
151
109
  throw error;
152
110
  }
153
- if (error instanceof import_core4.DextoRuntimeError) {
154
- throw import_core2.ToolError.validationFailed("edit_file", error.message, {
155
- file_path,
111
+ if (error instanceof import_core.DextoRuntimeError) {
112
+ throw import_core.ToolError.validationFailed("edit_file", error.message, {
113
+ file_path: resolvedPath,
156
114
  originalErrorCode: error.code
157
115
  });
158
116
  }
159
117
  return null;
160
118
  }
161
119
  },
162
- execute: async (input, context) => {
120
+ async execute(input, context) {
121
+ const resolvedFileSystemService = await getFileSystemService(context);
163
122
  const { file_path, old_string, new_string, replace_all } = input;
164
- if (context?.toolCallId && previewContentHashCache.has(context.toolCallId)) {
165
- const expectedHash = previewContentHashCache.get(context.toolCallId);
166
- previewContentHashCache.delete(context.toolCallId);
167
- let currentContent;
168
- try {
169
- const currentFile = await fileSystemService.readFile(file_path);
170
- currentContent = currentFile.content;
171
- } catch (error) {
172
- if (error instanceof import_core4.DextoRuntimeError && error.code === import_error_codes.FileSystemErrorCode.FILE_NOT_FOUND) {
173
- throw import_core2.ToolError.fileModifiedSincePreview("edit_file", file_path);
123
+ const { path: resolvedPath } = (0, import_directory_approval.resolveFilePath)(
124
+ resolvedFileSystemService.getWorkingDirectory(),
125
+ file_path
126
+ );
127
+ const toolCallId = context.toolCallId;
128
+ if (toolCallId) {
129
+ const expectedHash = previewContentHashCache.get(toolCallId);
130
+ if (expectedHash === void 0) {
131
+ } else {
132
+ previewContentHashCache.delete(toolCallId);
133
+ let currentContent;
134
+ try {
135
+ const currentFile = await resolvedFileSystemService.readFile(resolvedPath);
136
+ currentContent = currentFile.content;
137
+ } catch (error) {
138
+ if (error instanceof import_core.DextoRuntimeError && error.code === import_error_codes.FileSystemErrorCode.FILE_NOT_FOUND) {
139
+ throw import_core.ToolError.fileModifiedSincePreview("edit_file", resolvedPath);
140
+ }
141
+ throw error;
142
+ }
143
+ const currentHash = computeContentHash(currentContent);
144
+ if (expectedHash !== currentHash) {
145
+ throw import_core.ToolError.fileModifiedSincePreview("edit_file", resolvedPath);
174
146
  }
175
- throw error;
176
- }
177
- const currentHash = computeContentHash(currentContent);
178
- if (expectedHash !== currentHash) {
179
- throw import_core2.ToolError.fileModifiedSincePreview("edit_file", file_path);
180
147
  }
181
148
  }
182
- const result = await fileSystemService.editFile(file_path, {
149
+ const result = await resolvedFileSystemService.editFile(resolvedPath, {
183
150
  oldString: old_string,
184
151
  newString: new_string,
185
152
  replaceAll: replace_all
186
153
  });
187
154
  const _display = generateDiffPreview(
188
- file_path,
155
+ resolvedPath,
189
156
  result.originalContent,
190
157
  result.newContent
191
158
  );
@@ -197,7 +164,7 @@ function createEditFileTool(options) {
197
164
  _display
198
165
  };
199
166
  }
200
- };
167
+ });
201
168
  }
202
169
  // Annotate the CommonJS export names for ESM import in node:
203
170
  0 && (module.exports = {
@@ -1,5 +1,6 @@
1
- import { InternalTool } from '@dexto/core';
2
- import { FileToolOptions } from './file-tool-types.cjs';
1
+ import { z } from 'zod';
2
+ import { Tool } from '@dexto/core';
3
+ import { FileSystemServiceGetter } from './file-tool-types.cjs';
3
4
  import './filesystem-service.cjs';
4
5
  import './types.cjs';
5
6
 
@@ -9,9 +10,25 @@ import './types.cjs';
9
10
  * Internal tool for editing files by replacing text (requires approval)
10
11
  */
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
+ }>;
12
29
  /**
13
30
  * Create the edit_file internal tool with directory approval support
14
31
  */
15
- declare function createEditFileTool(options: FileToolOptions): InternalTool;
32
+ declare function createEditFileTool(getFileSystemService: FileSystemServiceGetter): Tool<typeof EditFileInputSchema>;
16
33
 
17
34
  export { createEditFileTool };
@@ -1,17 +1,30 @@
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
1
  /**
7
2
  * Edit File Tool
8
3
  *
9
4
  * Internal tool for editing files by replacing text (requires approval)
10
5
  */
11
-
6
+ import { z } from 'zod';
7
+ import type { Tool } from '@dexto/core';
8
+ import type { FileSystemServiceGetter } from './file-tool-types.js';
9
+ declare const EditFileInputSchema: z.ZodObject<{
10
+ file_path: z.ZodString;
11
+ old_string: z.ZodString;
12
+ new_string: z.ZodString;
13
+ replace_all: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
14
+ }, "strict", z.ZodTypeAny, {
15
+ file_path: string;
16
+ old_string: string;
17
+ new_string: string;
18
+ replace_all: boolean;
19
+ }, {
20
+ file_path: string;
21
+ old_string: string;
22
+ new_string: string;
23
+ replace_all?: boolean | undefined;
24
+ }>;
12
25
  /**
13
26
  * Create the edit_file internal tool with directory approval support
14
27
  */
15
- declare function createEditFileTool(options: FileToolOptions): InternalTool;
16
-
17
- export { createEditFileTool };
28
+ export declare function createEditFileTool(getFileSystemService: FileSystemServiceGetter): Tool<typeof EditFileInputSchema>;
29
+ export {};
30
+ //# sourceMappingURL=edit-file-tool.d.ts.map
@@ -0,0 +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,18 +1,15 @@
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 { DextoRuntimeError, ToolError, ToolErrorCode, defineTool } from "@dexto/core";
9
5
  import { FileSystemErrorCode } from "./error-codes.js";
6
+ import { createDirectoryAccessApprovalHandlers, resolveFilePath } from "./directory-approval.js";
10
7
  const previewContentHashCache = /* @__PURE__ */ new Map();
11
8
  function computeContentHash(content) {
12
9
  return createHash("sha256").update(content, "utf8").digest("hex");
13
10
  }
14
11
  const EditFileInputSchema = z.object({
15
- file_path: z.string().describe("Absolute path to the file to edit"),
12
+ file_path: z.string().min(1).describe("Absolute path to the file to edit"),
16
13
  old_string: z.string().describe("Text to replace (must be unique unless replace_all is true)"),
17
14
  new_string: z.string().describe("Replacement text"),
18
15
  replace_all: z.boolean().optional().default(false).describe("Replace all occurrences (default: false, requires unique match)")
@@ -31,64 +28,35 @@ function generateDiffPreview(filePath, originalContent, newContent) {
31
28
  deletions
32
29
  };
33
30
  }
34
- function createEditFileTool(options) {
35
- const { fileSystemService, directoryApproval } = options;
36
- let pendingApprovalParentDir;
37
- return {
31
+ function createEditFileTool(getFileSystemService) {
32
+ return defineTool({
38
33
  id: "edit_file",
34
+ displayName: "Update",
35
+ aliases: ["edit"],
39
36
  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
37
  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
- },
38
+ ...createDirectoryAccessApprovalHandlers({
39
+ toolName: "edit_file",
40
+ operation: "edit",
41
+ getFileSystemService,
42
+ resolvePaths: (input, fileSystemService) => resolveFilePath(fileSystemService.getWorkingDirectory(), input.file_path)
43
+ }),
81
44
  /**
82
45
  * Generate preview for approval UI - shows diff without modifying file
83
46
  * Throws ToolError.validationFailed() for validation errors (file not found, string not found)
84
47
  * Stores content hash for change detection in execute phase.
85
48
  */
86
- generatePreview: async (input, context) => {
49
+ async generatePreview(input, context) {
87
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
+ );
88
56
  try {
89
- const originalFile = await fileSystemService.readFile(file_path);
57
+ const originalFile = await resolvedFileSystemService.readFile(resolvedPath);
90
58
  const originalContent = originalFile.content;
91
- if (context?.toolCallId) {
59
+ if (context.toolCallId) {
92
60
  previewContentHashCache.set(
93
61
  context.toolCallId,
94
62
  computeContentHash(originalContent)
@@ -100,7 +68,7 @@ function createEditFileTool(options) {
100
68
  throw ToolError.validationFailed(
101
69
  "edit_file",
102
70
  `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 }
71
+ { file_path: resolvedPath, occurrences }
104
72
  );
105
73
  }
106
74
  }
@@ -109,50 +77,59 @@ function createEditFileTool(options) {
109
77
  throw ToolError.validationFailed(
110
78
  "edit_file",
111
79
  `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) }
80
+ { file_path: resolvedPath, old_string_preview: old_string.slice(0, 100) }
113
81
  );
114
82
  }
115
- return generateDiffPreview(file_path, originalContent, newContent);
83
+ return generateDiffPreview(resolvedPath, originalContent, newContent);
116
84
  } catch (error) {
117
85
  if (error instanceof DextoRuntimeError && error.code === ToolErrorCode.VALIDATION_FAILED) {
118
86
  throw error;
119
87
  }
120
88
  if (error instanceof DextoRuntimeError) {
121
89
  throw ToolError.validationFailed("edit_file", error.message, {
122
- file_path,
90
+ file_path: resolvedPath,
123
91
  originalErrorCode: error.code
124
92
  });
125
93
  }
126
94
  return null;
127
95
  }
128
96
  },
129
- execute: async (input, context) => {
97
+ async execute(input, context) {
98
+ const resolvedFileSystemService = await getFileSystemService(context);
130
99
  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);
100
+ const { path: resolvedPath } = resolveFilePath(
101
+ resolvedFileSystemService.getWorkingDirectory(),
102
+ file_path
103
+ );
104
+ const toolCallId = context.toolCallId;
105
+ if (toolCallId) {
106
+ const expectedHash = previewContentHashCache.get(toolCallId);
107
+ if (expectedHash === void 0) {
108
+ } else {
109
+ previewContentHashCache.delete(toolCallId);
110
+ let currentContent;
111
+ try {
112
+ const currentFile = await resolvedFileSystemService.readFile(resolvedPath);
113
+ currentContent = currentFile.content;
114
+ } catch (error) {
115
+ if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.FILE_NOT_FOUND) {
116
+ throw ToolError.fileModifiedSincePreview("edit_file", resolvedPath);
117
+ }
118
+ throw error;
119
+ }
120
+ const currentHash = computeContentHash(currentContent);
121
+ if (expectedHash !== currentHash) {
122
+ throw ToolError.fileModifiedSincePreview("edit_file", resolvedPath);
141
123
  }
142
- throw error;
143
- }
144
- const currentHash = computeContentHash(currentContent);
145
- if (expectedHash !== currentHash) {
146
- throw ToolError.fileModifiedSincePreview("edit_file", file_path);
147
124
  }
148
125
  }
149
- const result = await fileSystemService.editFile(file_path, {
126
+ const result = await resolvedFileSystemService.editFile(resolvedPath, {
150
127
  oldString: old_string,
151
128
  newString: new_string,
152
129
  replaceAll: replace_all
153
130
  });
154
131
  const _display = generateDiffPreview(
155
- file_path,
132
+ resolvedPath,
156
133
  result.originalContent,
157
134
  result.newContent
158
135
  );
@@ -164,7 +141,7 @@ function createEditFileTool(options) {
164
141
  _display
165
142
  };
166
143
  }
167
- };
144
+ });
168
145
  }
169
146
  export {
170
147
  createEditFileTool