diff --git a/src/clojure_mcp/tools/file_write/core.clj b/src/clojure_mcp/tools/file_write/core.clj index 292df45f..55906a03 100644 --- a/src/clojure_mcp/tools/file_write/core.clj +++ b/src/clojure_mcp/tools/file_write/core.clj @@ -2,22 +2,22 @@ "Core implementation for the file-write tool. This namespace contains the pure functionality without any MCP-specific code." (:require - [clojure.java.io :as io] - [clojure-mcp.tools.form-edit.pipeline :as pipeline] - [clojure-mcp.utils.diff :as diff-utils] - [clojure-mcp.linting :as linting] - [clojure-mcp.utils.valid-paths :as valid-paths] - [rewrite-clj.zip :as z])) + [clojure.java.io :as io] + [clojure-mcp.tools.form-edit.pipeline :as pipeline] + [clojure-mcp.utils.diff :as diff-utils] + [clojure-mcp.linting :as linting] + [clojure-mcp.utils.valid-paths :as valid-paths] + [rewrite-clj.zip :as z])) - (defn is-clojure-file? - "Check if a file is a Clojure-related file based on its extension or Babashka shebang. +(defn is-clojure-file? + "Check if a file is a Clojure-related file based on its extension or Babashka shebang. Parameters: - file-path: Path to the file to check Returns true for Clojure extensions (.clj, .cljs, .cljc, .edn, .bb) or files with a `bb` shebang." - [file-path] - (boolean (valid-paths/clojure-file? file-path))) + [file-path] + (boolean (valid-paths/clojure-file? file-path))) (defn write-clojure-file "Write content to a Clojure file, with linting, formatting, and diffing. @@ -25,10 +25,11 @@ Parameters: - file-path: Validated path to the file to write - content: Content to write to the file + - dry_run: Optional preview mode ('diff' or 'new-source') Returns: - A map with :error, :type, :file-path, and :diff keys" - [nrepl-client-atom file-path content] + [nrepl-client-atom file-path content dry_run] (let [file (io/file file-path) file-exists? (.exists file) old-content (if file-exists? (slurp file) "") @@ -50,16 +51,35 @@ pipeline/format-source ;; Format the content pipeline/generate-diff ;; Generate diff between old and new content pipeline/determine-file-type ;; Determine if creating or updating - pipeline/save-file)] ;; Save the file and get offsets + ;; Conditionally skip file save if dry_run is set + (fn [ctx] + (if dry_run + ctx + (pipeline/save-file ctx))))] ;; Format the result for tool consumption (if (::pipeline/error result) {:error true :message (::pipeline/message result)} - {:error false - :type (::pipeline/type result) - :file-path (::pipeline/file-path result) - :diff (::pipeline/diff result)}))) + (cond + ;; Return new-source for dry_run="new-source" + (= dry_run "new-source") + {:error false + :dry_run "new-source" + :new-source (::pipeline/output-source result)} + + ;; Return just diff for dry_run="diff" + (= dry_run "diff") + {:error false + :dry_run "diff" + :diff (::pipeline/diff result)} + + ;; Return full result for normal operation + :else + {:error false + :type (::pipeline/type result) + :file-path (::pipeline/file-path result) + :diff (::pipeline/diff result)})))) (defn write-text-file "Write content to a non-Clojure text file, with diffing but no linting or formatting. @@ -67,10 +87,11 @@ Parameters: - file-path: Validated path to the file to write - content: Content to write to the file + - dry_run: Optional preview mode ('diff' or 'new-source') Returns: - A map with :error, :type, :file-path, and :diff keys" - [file-path content] + [file-path content dry_run] (try (let [file (io/file file-path) file-exists? (.exists file) @@ -82,13 +103,27 @@ (diff-utils/generate-unified-diff old-content content)) "")] - ;; Write the content directly - (spit file content) + (cond + ;; Return new-source if dry_run="new-source" + (= dry_run "new-source") + {:error false + :dry_run "new-source" + :new-source content} + + ;; Return just diff for dry_run="diff" + (= dry_run "diff") + {:error false + :dry_run "diff" + :diff diff} - {:error false - :type (if file-exists? "update" "create") - :file-path file-path - :diff diff}) + ;; Normal operation - write and return full result + :else + (do + (spit file content) + {:error false + :type (if file-exists? "update" "create") + :file-path file-path + :diff diff}))) (catch Exception e {:error true :message (str "Error writing file: " (.getMessage e))}))) @@ -101,10 +136,11 @@ Parameters: - file-path: Validated path to the file to write - content: Content to write to the file + - dry_run: Optional preview mode ('diff' or 'new-source') Returns: - A map with :error, :type, :file-path, and :diff keys" - [nrepl-client-atom file-path content] + [nrepl-client-atom file-path content dry_run] (if (is-clojure-file? file-path) - (write-clojure-file nrepl-client-atom file-path content) - (write-text-file file-path content))) + (write-clojure-file nrepl-client-atom file-path content dry_run) + (write-text-file file-path content dry_run))) diff --git a/src/clojure_mcp/tools/file_write/tool.clj b/src/clojure_mcp/tools/file_write/tool.clj index b630ab7d..5d86cc66 100644 --- a/src/clojure_mcp/tools/file_write/tool.clj +++ b/src/clojure_mcp/tools/file_write/tool.clj @@ -63,7 +63,7 @@ Before using this tool: :required [:file_path :content]}) (defmethod tool-system/validate-inputs :file-write [{:keys [nrepl-client-atom]} inputs] - (let [{:keys [file_path content]} inputs + (let [{:keys [file_path content dry_run]} inputs nrepl-client (and nrepl-client-atom @nrepl-client-atom)] (when-not file_path (throw (ex-info "Missing required parameter: file_path" {:inputs inputs}))) @@ -88,10 +88,11 @@ Before using this tool: ;; Return validated inputs with normalized path {:file-path validated-path - :content content}))) + :content content + :dry_run dry_run}))) (defmethod tool-system/execute-tool :file-write [{:keys [nrepl-client-atom]} inputs] - (let [{:keys [file-path content]} inputs + (let [{:keys [file-path content dry_run]} inputs ;; Capture original content - empty string for new files _ (when nrepl-client-atom (let [file (io/file file-path)] @@ -105,18 +106,31 @@ Before using this tool: nrepl-client-atom file-path "")))) - result (core/write-file nrepl-client-atom file-path content)] + result (core/write-file nrepl-client-atom file-path content dry_run)] ;; Update the timestamp if write was successful and we have a client atom - (when (and nrepl-client-atom (not (:error result))) + (when (and nrepl-client-atom (not (:error result)) (not dry_run)) (file-timestamps/update-file-timestamp-to-current-mtime! nrepl-client-atom file-path)) result)) (defmethod tool-system/format-results :file-write [_ result] - (if (:error result) + (cond ;; If there's an error, return the error message + (:error result) {:result [(:message result)] :error true} - ;; Otherwise, format a successful result + + ;; Check if this is a dry_run with new-source + (:new-source result) + {:result [(:new-source result)] + :error false} + + ;; Check if this is a dry_run with diff only (no file-path) + (and (:diff result) (not (:file-path result))) + {:result [(:diff result)] + :error false} + + ;; Otherwise, format a successful result with preamble + :else (let [file-type (if (core/is-clojure-file? (:file-path result)) "Clojure" "Text") response (str file-type " file " (:type result) "d: " (:file-path result))] (if (seq (:diff result)) diff --git a/test/clojure_mcp/tools/file_write/core_test.clj b/test/clojure_mcp/tools/file_write/core_test.clj index 1a30f288..30822c70 100644 --- a/test/clojure_mcp/tools/file_write/core_test.clj +++ b/test/clojure_mcp/tools/file_write/core_test.clj @@ -66,7 +66,7 @@ (testing "Creating a new text file" (let [new-file (io/file *test-dir* "new-text-file.txt") content "This is a brand new file." - result (file-write-core/write-text-file (.getPath new-file) content)] + result (file-write-core/write-text-file (.getPath new-file) content nil)] (is (not (:error result))) (is (= "create" (:type result))) (is (= (.getPath new-file) (:file-path result))) @@ -79,7 +79,7 @@ (let [path (.getPath *test-txt-file*) original-content (slurp *test-txt-file*) new-content "This is updated content.\nWith new lines." - result (file-write-core/write-text-file path new-content)] + result (file-write-core/write-text-file path new-content nil)] (is (not (:error result))) (is (= "update" (:type result))) (is (= path (:file-path result))) @@ -93,7 +93,8 @@ result (file-write-core/write-clojure-file test-utils/*nrepl-client-atom* (.getPath new-file) - content)] + content + nil)] (is (not (:error result))) (is (= "create" (:type result))) (is (= (.getPath new-file) (:file-path result))) @@ -107,7 +108,8 @@ result (file-write-core/write-clojure-file test-utils/*nrepl-client-atom* path - new-content)] + new-content + nil)] (is (not (:error result))) (is (= "update" (:type result))) (is (= path (:file-path result))) @@ -120,7 +122,8 @@ result (file-write-core/write-clojure-file test-utils/*nrepl-client-atom* path - unformatted-content)] + unformatted-content + nil)] (is (not (:error result))) (is (= "update" (:type result))) (is (not (str/includes? (slurp *test-clj-file*) "poorly-formatted-fn[x]"))) @@ -132,7 +135,8 @@ result (file-write-core/write-clojure-file test-utils/*nrepl-client-atom* path - content-with-missing-paren)] + content-with-missing-paren + nil)] (is (not (:error result))) (is (= "update" (:type result))) (let [saved-content (slurp *test-clj-file*)] @@ -146,7 +150,8 @@ result (file-write-core/write-clojure-file test-utils/*nrepl-client-atom* path - content-with-mismatched-brackets)] + content-with-mismatched-brackets + nil)] (is (not (:error result))) (is (= "update" (:type result))) (let [saved-content (slurp *test-clj-file*)] @@ -161,7 +166,8 @@ result (file-write-core/write-clojure-file test-utils/*nrepl-client-atom* path - content-with-syntax-error)] + content-with-syntax-error + nil)] ;; The test should fail with a specific error (is (:error result) "Should have error for non-repairable syntax error") (when (:message result) @@ -176,7 +182,8 @@ content "(ns dispatch.test)" result (file-write-core/write-file test-utils/*nrepl-client-atom* (.getPath clj-file) - content)] + content + nil)] (is (not (:error result))) (is (= "create" (:type result))) (is (.exists clj-file)) @@ -187,7 +194,8 @@ content "Plain text content" result (file-write-core/write-file test-utils/*nrepl-client-atom* (.getPath txt-file) - content)] + content + nil)] (is (not (:error result))) (is (= "create" (:type result))) (is (.exists txt-file))