|
14 | 14 |
|
15 | 15 | (set! *warn-on-reflection* true) |
16 | 16 |
|
17 | | -(defn ^:private allowed-path? [db path] |
18 | | - (some #(fs/starts-with? path (shared/uri->filename (:uri %))) |
19 | | - (:workspace-folders db))) |
20 | | - |
21 | | -(defn ^:private path-validations [db] |
22 | | - [["path" fs/exists? "$path is not a valid path"] |
23 | | - ["path" (partial allowed-path? db) (str "Access denied - path $path outside allowed directories: " (tools.util/workspace-roots-strs db))]]) |
| 17 | +(defn ^:private path-validations [] |
| 18 | + [["path" fs/exists? "$path is not a valid path"]]) |
24 | 19 |
|
25 | 20 | (def ^:private directory-tree-max-depth 10) |
26 | 21 |
|
|
35 | 30 |
|
36 | 31 | (defn ^:private directory-tree [arguments {:keys [db config]}] |
37 | 32 | (let [path (delay (fs/canonicalize (get arguments "path")))] |
38 | | - (or (tools.util/invalid-arguments arguments (path-validations db)) |
| 33 | + (or (tools.util/invalid-arguments arguments (path-validations)) |
39 | 34 | (let [max-depth (or (get arguments "max_depth") directory-tree-max-depth) |
40 | 35 | dir-count* (atom 0) |
41 | 36 | file-count* (atom 0) |
|
69 | 64 | summary (format "%d directories, %d files" @dir-count* @file-count*)] |
70 | 65 | (tools.util/single-text-content (str body "\n\n" summary))))))) |
71 | 66 |
|
72 | | -(defn ^:private read-file [arguments {:keys [db config]}] |
73 | | - (or (tools.util/invalid-arguments arguments (concat (path-validations db) |
| 67 | +(defn ^:private read-file [arguments {:keys [config]}] |
| 68 | + (or (tools.util/invalid-arguments arguments (concat (path-validations) |
74 | 69 | [["path" fs/readable? "File $path is not readable"] |
75 | 70 | ["path" (complement fs/directory?) "$path is a directory, not a file"]])) |
76 | 71 | (let [line-offset (or (get arguments "line_offset") 0) |
|
99 | 94 | (str "Reading file " (fs/file-name (fs/file path))) |
100 | 95 | "Reading file")) |
101 | 96 |
|
102 | | -(defn ^:private write-file [arguments {:keys [db]}] |
103 | | - (or (tools.util/invalid-arguments arguments [["path" (partial allowed-path? db) (str "Access denied - path $path outside allowed directories: " (tools.util/workspace-roots-strs db))]]) |
104 | | - (let [path (get arguments "path") |
105 | | - content (get arguments "content")] |
106 | | - (fs/create-dirs (fs/parent (fs/path path))) |
107 | | - (spit path content) |
108 | | - (tools.util/single-text-content (format "Successfully wrote to %s" path))))) |
| 97 | +(defn ^:private write-file [arguments _] |
| 98 | + (let [path (get arguments "path") |
| 99 | + content (get arguments "content")] |
| 100 | + (fs/create-dirs (fs/parent (fs/path path))) |
| 101 | + (spit path content) |
| 102 | + (tools.util/single-text-content (format "Successfully wrote to %s" path)))) |
109 | 103 |
|
110 | 104 | (defn ^:private write-file-summary [{:keys [args]}] |
111 | 105 | (if-let [path (get args "path")] |
|
178 | 172 |
|
179 | 173 | Returns matching file paths, prioritizing by modification time when possible. |
180 | 174 | Validates that the search path is within allowed workspace directories." |
181 | | - [arguments {:keys [db]}] |
182 | | - (or (tools.util/invalid-arguments arguments (concat (path-validations db) |
| 175 | + [arguments _] |
| 176 | + (or (tools.util/invalid-arguments arguments (concat (path-validations) |
183 | 177 | [["path" fs/readable? "File $path is not readable"] |
184 | 178 | ["pattern" #(and % (not (string/blank? %))) "Invalid content regex pattern '$pattern'"] |
185 | 179 | ["include" #(or (nil? %) (not (string/blank? %))) "Invalid file pattern '$include'"] |
|
245 | 239 | (text-match/apply-content-change-to-string file-content original-content new-content all? path) |
246 | 240 | (smart-edit/apply-smart-edit file-content original-content new-content path))) |
247 | 241 |
|
| 242 | + |
248 | 243 | (defn ^:private edit-file [arguments {:keys [db]}] |
249 | | - (or (tools.util/invalid-arguments arguments (concat (path-validations db) |
| 244 | + (or (tools.util/invalid-arguments arguments (concat (path-validations) |
250 | 245 | [["path" fs/readable? "File $path is not readable"]])) |
251 | 246 | (let [path (get arguments "path") |
252 | 247 | original-content (get arguments "original_content") |
|
268 | 263 | (handle-file-change-result {:error :conflict} path nil))))) |
269 | 264 | (handle-file-change-result result path nil))))) |
270 | 265 |
|
271 | | -(defn ^:private preview-file-change [arguments {:keys [db]}] |
272 | | - (or (tools.util/invalid-arguments arguments [["path" (partial allowed-path? db) (str "Access denied - path $path outside allowed directories: " (tools.util/workspace-roots-strs db))]]) |
273 | | - (let [path (get arguments "path") |
274 | | - original-content (get arguments "original_content") |
275 | | - new-content (get arguments "new_content") |
276 | | - all? (boolean (get arguments "all_occurrences")) |
277 | | - file-exists? (fs/exists? path)] |
278 | | - (cond |
279 | | - file-exists? |
280 | | - (let [result (apply-file-edit-strategy (slurp path) original-content new-content all? path)] |
281 | | - (handle-file-change-result result path |
282 | | - (format "Change simulation completed for %s. Original file unchanged - preview only." path))) |
283 | | - |
284 | | - (and (not file-exists?) (= "" original-content)) |
285 | | - (tools.util/single-text-content (format "New file creation simulation completed for %s. File will be created - preview only." path)) |
286 | | - |
287 | | - :else |
288 | | - (tools.util/single-text-content |
289 | | - (format "Preview error for %s: For new files, original_content must be empty string (\"\"). Use markdown blocks during exploration, then eca_preview_file_change for final implementation only." |
290 | | - path) |
291 | | - :error))))) |
292 | | - |
293 | | -(defn ^:private move-file [arguments {:keys [db]}] |
294 | | - (let [workspace-dirs (tools.util/workspace-roots-strs db)] |
295 | | - (or (tools.util/invalid-arguments arguments [["source" fs/exists? "$source is not a valid path"] |
296 | | - ["source" (partial allowed-path? db) (str "Access denied - path $source outside allowed directories: " workspace-dirs)] |
297 | | - ["destination" (partial allowed-path? db) (str "Access denied - path $destination outside allowed directories: " workspace-dirs)] |
298 | | - ["destination" (complement fs/exists?) "Path $destination already exists"]]) |
299 | | - (let [source (get arguments "source") |
300 | | - destination (get arguments "destination")] |
301 | | - (fs/move source destination {:replace-existing false}) |
302 | | - (tools.util/single-text-content (format "Successfully moved %s to %s" source destination)))))) |
| 266 | +(defn ^:private preview-file-change [arguments _] |
| 267 | + (let [path (get arguments "path") |
| 268 | + original-content (get arguments "original_content") |
| 269 | + new-content (get arguments "new_content") |
| 270 | + all? (boolean (get arguments "all_occurrences")) |
| 271 | + file-exists? (fs/exists? path)] |
| 272 | + (cond |
| 273 | + file-exists? |
| 274 | + (let [result (apply-file-edit-strategy (slurp path) original-content new-content all? path)] |
| 275 | + (handle-file-change-result result path |
| 276 | + (format "Change simulation completed for %s. Original file unchanged - preview only." path))) |
| 277 | + |
| 278 | + (and (not file-exists?) (= "" original-content)) |
| 279 | + (tools.util/single-text-content (format "New file creation simulation completed for %s. File will be created - preview only." path)) |
| 280 | + |
| 281 | + :else |
| 282 | + (tools.util/single-text-content |
| 283 | + (format "Preview error for %s: For new files, original_content must be empty string (\"\")." |
| 284 | + path) |
| 285 | + :error)))) |
| 286 | + |
| 287 | +(defn ^:private move-file [arguments _] |
| 288 | + (or (tools.util/invalid-arguments arguments [["source" fs/exists? "$source is not a valid path"] |
| 289 | + ["destination" (complement fs/exists?) "Path $destination already exists"]]) |
| 290 | + (let [source (get arguments "source") |
| 291 | + destination (get arguments "destination")] |
| 292 | + (fs/move source destination {:replace-existing false}) |
| 293 | + (tools.util/single-text-content (format "Successfully moved %s to %s" source destination))))) |
303 | 294 |
|
304 | 295 | (def definitions |
305 | 296 | {"eca_directory_tree" |
|
311 | 302 | :description (format "Maximum depth to traverse (default: %s)" directory-tree-max-depth)}} |
312 | 303 | :required ["path"]} |
313 | 304 | :handler #'directory-tree |
| 305 | + :require-approval-fn (tools.util/require-approval-when-outside-workspace ["path"]) |
314 | 306 | :summary-fn (constantly "Listing file tree")} |
315 | 307 | "eca_read_file" |
316 | 308 | {:description (tools.util/read-tool-description "eca_read_file") |
|
323 | 315 | :description "Maximum lines to read (default: configured in tools.readFile.maxLines, defaults to 2000)"}} |
324 | 316 | :required ["path"]} |
325 | 317 | :handler #'read-file |
| 318 | + :require-approval-fn (tools.util/require-approval-when-outside-workspace ["path"]) |
326 | 319 | :summary-fn #'read-file-summary} |
327 | 320 | "eca_write_file" |
328 | 321 | {:description (tools.util/read-tool-description "eca_write_file") |
|
333 | 326 | :description "The complete content to write to the file"}} |
334 | 327 | :required ["path" "content"]} |
335 | 328 | :handler #'write-file |
| 329 | + :require-approval-fn (tools.util/require-approval-when-outside-workspace ["path"]) |
336 | 330 | :summary-fn #'write-file-summary} |
337 | 331 | "eca_edit_file" |
338 | 332 | {:description (tools.util/read-tool-description "eca_edit_file") |
|
347 | 341 | :description "Whether to replace all occurrences of the file or just the first one (default)"}} |
348 | 342 | :required ["path" "original_content" "new_content"]} |
349 | 343 | :handler #'edit-file |
| 344 | + :require-approval-fn (tools.util/require-approval-when-outside-workspace ["path"]) |
350 | 345 | :summary-fn (constantly "Editing file")} |
351 | 346 | "eca_preview_file_change" |
352 | 347 | {:description (tools.util/read-tool-description "eca_preview_file_change") |
|
361 | 356 | :description "Whether to preview replacing all occurrences or just the first one (default)"}} |
362 | 357 | :required ["path" "original_content" "new_content"]} |
363 | 358 | :handler #'preview-file-change |
| 359 | + :require-approval-fn (tools.util/require-approval-when-outside-workspace ["path"]) |
364 | 360 | :summary-fn (constantly "Previewing change")} |
365 | 361 | "eca_move_file" |
366 | 362 | {:description (tools.util/read-tool-description "eca_move_file") |
|
371 | 367 | :description "The new absolute file path to move to."}} |
372 | 368 | :required ["source" "destination"]} |
373 | 369 | :handler #'move-file |
| 370 | + :require-approval-fn (tools.util/require-approval-when-outside-workspace ["source" "destination"]) |
374 | 371 | :summary-fn (constantly "Moving file")} |
375 | 372 | "eca_grep" |
376 | 373 | {:description (tools.util/read-tool-description "eca_grep") |
|
385 | 382 | :description "Maximum number of results to return (default: 1000)"}} |
386 | 383 | :required ["path" "pattern"]} |
387 | 384 | :handler #'grep |
| 385 | + :require-approval-fn (tools.util/require-approval-when-outside-workspace ["path"]) |
388 | 386 | :summary-fn #'grep-summary}}) |
389 | 387 |
|
390 | 388 | (defmethod tools.util/tool-call-details-before-invocation :eca_edit_file [_name arguments _server _ctx] |
|
0 commit comments