diff --git a/src/clojure_mcp/tools/file_edit/pipeline.clj b/src/clojure_mcp/tools/file_edit/pipeline.clj index 64b8a079..9a3578b9 100644 --- a/src/clojure_mcp/tools/file_edit/pipeline.clj +++ b/src/clojure_mcp/tools/file_edit/pipeline.clj @@ -23,6 +23,8 @@ (s/def ::old-string string?) (s/def ::new-string string?) (s/def ::nrepl-client-atom (s/nilable #(instance? clojure.lang.Atom %))) + +(s/def ::dry-run (s/nilable #{"diff" "new-source"})) ;; Pipeline specific steps ;; Using check-file-modified from form-edit/pipeline instead @@ -117,15 +119,16 @@ - file-path: Path to the file to edit - old-string: String to replace - new-string: New string to insert - - nrepl-client-atom: Atom containing the nREPL client (optional) - - config: Optional tool configuration map + - dry-run: Optional string, either \"diff\" or \"new-source\" to skip actual file write + - config: Optional tool configuration map with :nrepl-client-atom Returns: - A context map with the result of the operation" - [file-path old-string new-string {:keys [nrepl-client-atom] :as config}] + [file-path old-string new-string dry_run {:keys [nrepl-client-atom] :as config}] (let [initial-ctx {::form-pipeline/file-path file-path ::old-string old-string ::new-string new-string + ::dry-run dry_run ::form-pipeline/nrepl-client-atom nrepl-client-atom ::form-pipeline/config config}] ;; Pipeline for existing file edit @@ -147,9 +150,14 @@ format-clojure-content ;; Format Clojure files automatically form-pipeline/determine-file-type ;; This will mark as "update" form-pipeline/generate-diff ;; Generate diff between old and new - form-pipeline/save-file ;; Save the file - form-pipeline/update-file-timestamp ;; Update the timestamp after save - form-pipeline/highlight-form))) ;; Update the timestamp after save ;; Update the timestamp after save + ;; Skip file operations if dry-run is set + (fn [ctx] + (if (::dry-run ctx) + ctx + (-> ctx + form-pipeline/save-file + form-pipeline/update-file-timestamp + form-pipeline/highlight-form)))))) ;; Update the timestamp after save ;; Update the timestamp after save ;; Format result for tool consumption (defn format-result @@ -159,17 +167,25 @@ - ctx: The final context map from the pipeline Returns: - - A map with :error, :message, and :diff keys, and potentially :repaired" + - A map with :error, :message, and :diff or :new-source keys, and potentially :repaired" [ctx] (if (::form-pipeline/error ctx) {:error true :message (::form-pipeline/message ctx)} - (cond-> {:error false - :diff (::form-pipeline/diff ctx) - :type (::form-pipeline/type ctx)} - ;; Include repaired flag if present - (::form-pipeline/repaired ctx) - (assoc :repaired true)))) + (let [dry_run (::dry-run ctx)] + (cond-> {:error false + :type (::form-pipeline/type ctx)} + ;; Include repaired flag if present + (::form-pipeline/repaired ctx) + (assoc :repaired true) + + ;; Return new-source if dry-run is "new-source" + (= dry_run "new-source") + (assoc :new-source (::form-pipeline/output-source ctx)) + + ;; Otherwise return diff (default behavior and for "diff" dry-run) + (not= dry_run "new-source") + (assoc :diff (::form-pipeline/diff ctx)))))) (comment ;; === Examples of using the file-edit pipeline directly === diff --git a/src/clojure_mcp/tools/file_edit/tool.clj b/src/clojure_mcp/tools/file_edit/tool.clj index 68a0a405..93488b40 100644 --- a/src/clojure_mcp/tools/file_edit/tool.clj +++ b/src/clojure_mcp/tools/file_edit/tool.clj @@ -81,16 +81,16 @@ To make a file edit, provide the file_path, old_string (the text to replace), an :new_string new_string)))) (defmethod tool-system/execute-tool :file-edit [{:keys [nrepl-client-atom] :as tool} inputs] - (let [{:keys [file_path old_string new_string]} inputs - result (pipeline/file-edit-pipeline file_path old_string new_string tool)] + (let [{:keys [file_path old_string new_string dry_run]} inputs + result (pipeline/file-edit-pipeline file_path old_string new_string dry_run tool)] (pipeline/format-result result))) -(defmethod tool-system/format-results :file-edit [_ {:keys [error message diff type repaired]}] +(defmethod tool-system/format-results :file-edit [_ {:keys [error message diff new-source type repaired]}] (if error {:error true :result [message]} (cond-> {:error false - :result [diff] + :result [(or new-source diff)] :type type} ;; Include repaired flag if present repaired (assoc :repaired true)))) diff --git a/src/clojure_mcp/tools/form_edit/combined_edit_tool.clj b/src/clojure_mcp/tools/form_edit/combined_edit_tool.clj index 6f326c1f..aa69b298 100644 --- a/src/clojure_mcp/tools/form_edit/combined_edit_tool.clj +++ b/src/clojure_mcp/tools/form_edit/combined_edit_tool.clj @@ -126,8 +126,7 @@ Note: For `defmethod` forms, be sure to include the dispatch value (`area :recta ;; Validate inputs implementation (defmethod tool-system/validate-inputs :clojure-edit-form [{:keys [nrepl-client-atom]} inputs] (let [file-path (validate-file-path inputs nrepl-client-atom) - ;; Accept form_identifier but map it to form_name internally for compatibility - {:keys [form_identifier form_type operation content]} inputs + {:keys [form_identifier form_type operation content dry_run]} inputs form_name form_identifier] (when-not form_identifier (throw (ex-info "Missing required parameter: form_identifier" @@ -145,30 +144,30 @@ Note: For `defmethod` forms, be sure to include the dispatch value (`area :recta (when-not content (throw (ex-info "Missing required parameter: content" {:inputs inputs}))) - ;; Return validated inputs with form_name for backward compatibility {:file_path file-path :form_name form_name :form_type form_type :operation operation - :content content})) + :content content + :dry_run dry_run})) ;; Execute tool implementation (defmethod tool-system/execute-tool :clojure-edit-form [{:keys [nrepl-client-atom] :as tool} inputs] - (let [{:keys [file_path form_name form_type operation content]} inputs + (let [{:keys [file_path form_name form_type operation content dry_run]} inputs edit-type (case operation "replace" :replace "insert_before" :before "insert_after" :after) - result (pipeline/edit-form-pipeline file_path form_name form_type content edit-type tool) + result (pipeline/edit-form-pipeline file_path form_name form_type content edit-type dry_run tool) formatted-result (pipeline/format-result result)] formatted-result)) ;; Format results implementation -(defmethod tool-system/format-results :clojure-edit-form [_ {:keys [error message diff]}] +(defmethod tool-system/format-results :clojure-edit-form [_ {:keys [error message diff new-source]}] (if error {:result [message] :error true} - {:result [diff] + {:result [(or new-source diff)] :error false})) ;; Tool registration function diff --git a/src/clojure_mcp/tools/form_edit/pipeline.clj b/src/clojure_mcp/tools/form_edit/pipeline.clj index 4d7030bd..ea30158f 100644 --- a/src/clojure_mcp/tools/form_edit/pipeline.clj +++ b/src/clojure_mcp/tools/form_edit/pipeline.clj @@ -43,14 +43,15 @@ (s/def ::nrepl-client-atom (s/nilable #(instance? clojure.lang.Atom %))) -;; Context map that flows through the pipeline +(s/def ::dry-run (s/nilable #{"diff" "new-source"})) + (s/def ::context (s/keys :req [::file-path] :opt [::source ::old-content ::new-source-code ::top-level-def-name ::top-level-def-type ::edit-type ::error ::message ::zloc ::offsets ::lint-result ::docstring ::comment-substring ::new-content ::expand-symbols - ::diff ::type ::output-source ::nrepl-client-atom])) + ::diff ::type ::output-source ::nrepl-client-atom ::dry-run])) ;; Pipeline helper functions @@ -536,16 +537,27 @@ - ctx: The final context map from the pipeline Returns: - - A map with :error, :message, and possibly :offsets and :result keys" + - A map with :error, :message, and possibly :diff or :new-source, and :offsets keys" [ctx] (if (::error ctx) {:error true :message (::message ctx)} - (let [result-map {:error false}] + (let [dry_run (::dry-run ctx) + result-map {:error false}] (cond-> result-map (::offsets ctx) (assoc :offsets (::offsets ctx)) - (::output-source ctx) (assoc :result [(::output-source ctx)]) - (::diff ctx) (assoc :diff (::diff ctx)))))) + + ;; Return new-source if dry-run is "new-source" + (= dry_run "new-source") + (assoc :new-source (::output-source ctx)) + + ;; Otherwise return diff (default behavior and for "diff" dry-run) + (and (::diff ctx) (not= dry_run "new-source")) + (assoc :diff (::diff ctx)) + + ;; Legacy: include :result if output-source exists + (and (::output-source ctx) (not dry_run)) + (assoc :result [(::output-source ctx)]))))) ;; Pipeline function definitions @@ -558,16 +570,17 @@ - form-type: Type of form (e.g., 'defn', 'defmethod') - content-str: New content to insert - edit-type: Type of edit (:replace, :before, :after) - - nrepl-client-atom: Atom containing the nREPL client (optional) - - config: Optional tool configuration map + - dry-run: Optional string, either \"diff\" or \"new-source\" to skip actual file write + - config: Optional tool configuration map with :nrepl-client-atom Returns a context map with the result of the operation" - [file-path form-name form-type content-str edit-type {:keys [nrepl-client-atom] :as config}] + [file-path form-name form-type content-str edit-type dry_run {:keys [nrepl-client-atom] :as config}] (let [ctx {::file-path file-path ::top-level-def-name form-name ::top-level-def-type form-type ::new-source-code content-str ::edit-type edit-type + ::dry-run dry_run ::nrepl-client-atom nrepl-client-atom ::config config}] (thread-ctx @@ -587,9 +600,13 @@ format-source determine-file-type generate-diff - save-file - update-file-timestamp - highlight-form))) + (fn [ctx] + (if (::dry-run ctx) + ctx + (-> ctx + save-file + update-file-timestamp + highlight-form)))))) (defn docstring-edit-pipeline "Pipeline for editing a docstring in a file. @@ -769,17 +786,19 @@ - operation: The operation to perform (:replace, :insert-before, :insert-after) - replace-all: Whether to apply the operation to all occurrences - whitespace-sensitive: Whether to match forms exactly as written + - dry_run: Optional string, either \"diff\" or \"new-source\" to skip actual file write - config: Optional tool configuration map with nrepl-client-atom Returns: - A context map with the result of the operation" - [file-path match-form new-form operation replace-all whitespace-sensitive {:keys [nrepl-client-atom] :as config}] + [file-path match-form new-form operation replace-all whitespace-sensitive dry_run {:keys [nrepl-client-atom] :as config}] (let [ctx {::file-path file-path ::match-form match-form ::new-form new-form ::operation operation ::replace-all replace-all ::whitespace-sensitive whitespace-sensitive + ::dry-run dry_run ::nrepl-client-atom nrepl-client-atom ::config config}] (thread-ctx @@ -797,9 +816,13 @@ format-source determine-file-type generate-diff - save-file - update-file-timestamp - highlight-form))) + (fn [ctx] + (if (::dry-run ctx) + ctx + (-> ctx + save-file + update-file-timestamp + highlight-form)))))) (comment ;; Example usage of the pipelines diff --git a/src/clojure_mcp/tools/form_edit/tool.clj b/src/clojure_mcp/tools/form_edit/tool.clj index b7a9ff21..05f3a1b4 100644 --- a/src/clojure_mcp/tools/form_edit/tool.clj +++ b/src/clojure_mcp/tools/form_edit/tool.clj @@ -164,7 +164,7 @@ Note: For `defmethod` forms, be sure to include the dispatch value (`area :recta (defmethod tool-system/execute-tool :clojure-edit-replace-form [{:keys [nrepl-client-atom] :as tool} inputs] (let [{:keys [file_path form_name form_type content]} inputs - result (pipeline/edit-form-pipeline file_path form_name form_type content :replace tool) + result (pipeline/edit-form-pipeline file_path form_name form_type content :replace nil tool) formatted-result (pipeline/format-result result)] formatted-result)) @@ -256,7 +256,7 @@ Note: For `defmethod` forms, be sure to include the dispatch value (`area :recta (defmethod tool-system/execute-tool :clojure-edit-insert-before-form [{:keys [nrepl-client-atom] :as tool} inputs] (let [{:keys [file_path form_name form_type content]} inputs - result (pipeline/edit-form-pipeline file_path form_name form_type content :before tool) + result (pipeline/edit-form-pipeline file_path form_name form_type content :before nil tool) formatted-result (pipeline/format-result result)] formatted-result)) @@ -348,7 +348,7 @@ Note: For `defmethod` forms, be sure to include the dispatch value (`area :recta (defmethod tool-system/execute-tool :clojure-edit-insert-after-form [{:keys [nrepl-client-atom] :as tool} inputs] (let [{:keys [file_path form_name form_type content]} inputs - result (pipeline/edit-form-pipeline file_path form_name form_type content :after tool) + result (pipeline/edit-form-pipeline file_path form_name form_type content :after nil tool) formatted-result (pipeline/format-result result)] formatted-result)) @@ -545,7 +545,7 @@ For reliable results, use a unique substring that appears in only one comment bl (defmethod tool-system/validate-inputs :clojure-update-sexp [{:keys [nrepl-client-atom multi-op]} inputs] (let [file-path (validate-file-path inputs nrepl-client-atom) - {:keys [match_form new_form operation replace_all whitespace_sensitive]} inputs] + {:keys [match_form new_form operation replace_all whitespace_sensitive dry_run]} inputs] (when-not match_form (throw (ex-info "Missing required parameter: match_form" {:inputs inputs}))) @@ -567,16 +567,9 @@ For reliable results, use a unique substring that appears in only one comment bl (throw (ex-info "Bad parameter: match-form can not be a blank string." {:inputs inputs}))) - ;; TODO we can get more sophisticated here... we are handling - ;; code repairs deeper inside the actually tools evaluation and - ;; this prevents it. Also I think we can actually do comment - ;; replacement now but we might need special handling for that. - - ;; Special handling for empty string (when-not (str/blank? match_form) (try (let [parsed (p/parse-string-all match_form)] - ;; Check if there's at least one non-whitespace, non-comment node (when (zero? (count (n/child-sexprs parsed))) (throw (ex-info "match_form must contain at least one S-expression (not just comments or whitespace)" {:inputs inputs})))) @@ -587,13 +580,11 @@ For reliable results, use a unique substring that appears in only one comment bl {:inputs inputs})))))) (when-not (str/blank? new_form) - ;; Validate that new_form is valid Clojure code (try (p/parse-string-all new_form) (catch Exception e (throw (ex-info (str "Invalid Clojure code in new_form: " (.getMessage e)) {:inputs inputs}))))) - ;; Return validated inputs {:file_path file-path :match_form match_form :new_form new_form @@ -601,11 +592,11 @@ For reliable results, use a unique substring that appears in only one comment bl :replace_all (boolean (if (#{"insert_before" "insert_after"} operation) false (or replace_all false))) - :whitespace_sensitive (boolean (or whitespace_sensitive false))})) + :whitespace_sensitive (boolean (or whitespace_sensitive false)) + :dry_run dry_run})) (defmethod tool-system/execute-tool :clojure-update-sexp [{:keys [multi-op nrepl-client-atom] :as tool} inputs] - (let [{:keys [file_path match_form new_form operation replace_all whitespace_sensitive]} inputs - ;; Convert operation string to keyword for the pipeline + (let [{:keys [file_path match_form new_form operation replace_all whitespace_sensitive dry_run]} inputs operation-kw (if-not multi-op :replace (condp = operation @@ -613,15 +604,15 @@ For reliable results, use a unique substring that appears in only one comment bl "insert_before" :insert-before "insert_after" :insert-after)) result (pipeline/sexp-edit-pipeline - file_path match_form new_form operation-kw replace_all whitespace_sensitive tool) + file_path match_form new_form operation-kw replace_all whitespace_sensitive dry_run tool) formatted-result (pipeline/format-result result)] formatted-result)) -(defmethod tool-system/format-results :clojure-update-sexp [_ {:keys [error message diff]}] +(defmethod tool-system/format-results :clojure-update-sexp [_ {:keys [error message diff new-source]}] (if error {:result [message] :error true} - {:result [diff] + {:result [(or new-source diff)] :error false})) ;; Function to register the tool diff --git a/test/clojure_mcp/tools/form_edit/pipeline_test.clj b/test/clojure_mcp/tools/form_edit/pipeline_test.clj index 0ca6d16c..f04650d6 100644 --- a/test/clojure_mcp/tools/form_edit/pipeline_test.clj +++ b/test/clojure_mcp/tools/form_edit/pipeline_test.clj @@ -294,6 +294,7 @@ "defn" "(defn example-fn [x y]\n (* x y))" :replace + nil *nrepl-client-atom*) result (sut/format-result pipeline-result) file-content (slurp file-path)] @@ -316,6 +317,7 @@ "comment" "(comment some test comment)" :replace + nil *nrepl-client-atom*)] (is (true? (::sut/error pipeline-result))) (is (string? (::sut/message pipeline-result)))