Skip to content

Commit 2d0c3ec

Browse files
committed
Add move_file native tool
1 parent 56a9285 commit 2d0c3ec

4 files changed

Lines changed: 64 additions & 3 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ A Free and OpenSource editor-agnostic tool that aims to easily link LLMs <-> Edi
2727
- **Editor-agnostic** protocol for any editor to integrate.
2828
- **Single configuration**: Configure eca making it work the same in any editor.
2929
- **Chat** interface: ask questions, review diffs, work together with an agent in your codebase.
30-
- **Agentic** let LLM work as an agent with its built-in tools and MCPs you can configure.
30+
- **Agentic** let LLM work as an agent with its native tools and MCPs you can configure.
3131
- **Context** support: giving more details about your code to the LLM.
3232
- **Multi models**: Ollama local models, OpenAI, Anthropic, more on the way.
3333

docs/capabilities.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Provides access to filesystem under workspace root, listing and reading files an
1515
- `search_files`: search in a path for files matching a pattern.
1616
- `grep`: ripgrep/grep for paths with specified content.
1717
- `replace_in_file`: replace a text with another one in file.
18+
- `move_file`: move/rename a file.
1819

1920
### TODO - Shell
2021

src/eca/features/tools/filesystem.clj

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@
130130

131131
(defn ^:private grep
132132
"Searches for files containing patterns using regular expressions.
133-
133+
134134
This function provides a fast content search across files using three different
135135
backends depending on what's available:
136136
1. ripgrep (rg) - fastest, preferred when available
@@ -182,6 +182,17 @@
182182
(format "Successfully replaced content in %s." path))
183183
(format "Original content not found in %s" path))))))
184184

185+
(defn ^:private move-file [arguments db]
186+
(let [workspace-dirs (tools.util/workspace-roots-strs db)]
187+
(or (invalid-arguments arguments [["source" fs/exists? "$source is not a valid path"]
188+
["source" (partial allowed-path? db) (str "Access denied - path $source outside allowed directories: " workspace-dirs)]
189+
["destination" (partial allowed-path? db) (str "Access denied - path $destination outside allowed directories: " workspace-dirs)]
190+
["destination" (complement fs/exists?) "Path $destination already exists"]])
191+
(let [source (get arguments "source")
192+
destination (get arguments "destination")]
193+
(fs/move source destination {:replace-existing false})
194+
(single-text-content (format "Successfully moved %s to %s" source destination))))))
195+
185196
(def definitions
186197
{"list_directory"
187198
{:description (str "Get a detailed listing of all files and directories in a specified path. "
@@ -260,7 +271,19 @@
260271
:description "Whether to replace all occurences of the file or just the first one (default)"}}
261272
:required ["path" "original_content" "new_content"]}
262273
:handler #'replace-in-file}
263-
;; TODO move-file
274+
"move_file"
275+
{:description (str "Move or rename files and directories. Can move files between directories "
276+
"and rename them in a single operation. If the destination exists, the "
277+
"operation will fail. Works across different directories and can be used "
278+
"for simple renaming within the same directory. "
279+
"Both source and destination must be within the directories: $workspaceRoots.")
280+
:parameters {:type "object"
281+
:properties {"source" {:type "string"
282+
:description "The absolute origin file path to move."}
283+
"destination" {:type "string"
284+
:description "The new absolute file path to move to."}}
285+
:required ["source" "destination"]}
286+
:handler #'move-file}
264287
;; TODO write-file
265288
;; TODO delete-files
266289
})

test/eca/features/tools/filesystem_test.clj

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,40 @@
303303
(is (match?
304304
{(h/file-path "/project/foo/my-file.txt") "Hey, here is another-boring-text in this file! here as well: another-boring-text"}
305305
@file-content*)))))
306+
307+
(deftest move-file-test
308+
(testing "Not readable source path"
309+
(is (match?
310+
{:contents [{:type :text
311+
:error true
312+
:content (format "%s is not a valid path" (h/file-path "/foo/qux"))}]}
313+
(with-redefs [fs/exists? (constantly false)
314+
f.tools.filesystem/allowed-path? (constantly true)]
315+
((get-in f.tools.filesystem/definitions ["move_file" :handler])
316+
{"source" (h/file-path "/foo/qux")}
317+
{:workspace-folders [{:uri (h/file-uri "file:///foo/bar/baz") :name "foo"}]})))))
318+
(testing "Destination already exists"
319+
(is (match?
320+
{:contents [{:type :text
321+
:error true
322+
:content (format "Path %s already exists" (h/file-path "/foo/bar/other_file.clj"))}]}
323+
(with-redefs [fs/exists? (constantly true)
324+
f.tools.filesystem/allowed-path? (constantly true)]
325+
((get-in f.tools.filesystem/definitions ["move_file" :handler])
326+
{"source" (h/file-path "/foo/bar/some_file.clj")
327+
"destination" (h/file-path "/foo/bar/other_file.clj")}
328+
{:workspace-folders [{:uri (h/file-uri "file:///foo/bar") :name "foo"}]})))))
329+
(testing "Move successfully"
330+
(is (match?
331+
{:contents [{:type :text
332+
:error false
333+
:content (format "Successfully moved %s to %s"
334+
(h/file-path "/foo/bar/some_file.clj")
335+
(h/file-path "/foo/bar/other_file.clj"))}]}
336+
(with-redefs [fs/exists? (fn [path] (not (string/includes? path "other_file.clj")))
337+
f.tools.filesystem/allowed-path? (constantly true)
338+
fs/move (constantly true)]
339+
((get-in f.tools.filesystem/definitions ["move_file" :handler])
340+
{"source" (h/file-path "/foo/bar/some_file.clj")
341+
"destination" (h/file-path "/foo/bar/other_file.clj")}
342+
{:workspace-folders [{:uri (h/file-uri "file:///foo/bar") :name "foo"}]}))))))

0 commit comments

Comments
 (0)