|
3 | 3 | [babashka.fs :as fs] |
4 | 4 | [clojure.java.io :as io] |
5 | 5 | [clojure.java.shell :as shell] |
| 6 | + [eca.logger :as logger] |
6 | 7 | [clojure.string :as string] |
7 | 8 | [eca.diff :as diff] |
| 9 | + [eca.features.tools.text-match :as text-match] |
8 | 10 | [eca.features.tools.util :as tools.util] |
9 | 11 | [eca.shared :as shared])) |
10 | 12 |
|
|
203 | 205 | (format "Searching for '%s'" pattern) |
204 | 206 | "Searching for files")) |
205 | 207 |
|
206 | | -(defn file-change-full-content [path original-content new-content all?] |
207 | | - (let [original-full-content (slurp path) |
208 | | - new-full-content (if all? |
209 | | - (string/replace original-full-content original-content new-content) |
210 | | - (string/replace-first original-full-content original-content new-content))] |
211 | | - (when (string/includes? original-full-content original-content) |
212 | | - {:original-full-content original-full-content |
213 | | - :new-full-content new-full-content}))) |
| 208 | +(defn ^:private handle-file-change-result |
| 209 | + "Convert file-change-full-content result to appropriate tool response" |
| 210 | + [result path success-message] |
| 211 | + (cond |
| 212 | + (:new-full-content result) |
| 213 | + (tools.util/single-text-content success-message) |
214 | 214 |
|
215 | | -(defn ^:private change-file [arguments {:keys [db]} diff?] |
| 215 | + (= (:error result) :not-found) |
| 216 | + (tools.util/single-text-content (format "Original content not found in %s" path) :error) |
| 217 | + |
| 218 | + (= (:error result) :ambiguous) |
| 219 | + (tools.util/single-text-content |
| 220 | + (format "Ambiguous match - content appears %d times in %s. Provide more specific context to identify the exact location." |
| 221 | + (:match-count result) path) :error) |
| 222 | + |
| 223 | + :else |
| 224 | + (tools.util/single-text-content (format "Failed to process %s" path) :error))) |
| 225 | + |
| 226 | +(defn ^:private change-file [arguments {:keys [db]}] |
216 | 227 | (or (tools.util/invalid-arguments arguments (concat (path-validations db) |
217 | 228 | [["path" fs/readable? "File $path is not readable"]])) |
218 | 229 | (let [path (get arguments "path") |
219 | 230 | original-content (get arguments "original_content") |
220 | 231 | new-content (get arguments "new_content") |
221 | | - new-content (if diff? |
222 | | - (str "<<<<<<< HEAD\n" |
223 | | - original-content |
224 | | - "\n=======\n" |
225 | | - new-content |
226 | | - "\n>>>>>>> eca\n") |
227 | | - new-content) |
228 | | - all? (boolean (get arguments "all_occurrences"))] |
229 | | - (if-let [{:keys [new-full-content]} (file-change-full-content path original-content new-content all?)] |
| 232 | + all? (boolean (get arguments "all_occurrences")) |
| 233 | + result (text-match/apply-content-change-to-file path original-content new-content all?)] |
| 234 | + (if (:new-full-content result) |
230 | 235 | (do |
231 | | - (spit path new-full-content) |
232 | | - (tools.util/single-text-content (format "Successfully replaced content in %s." path))) |
233 | | - (tools.util/single-text-content (format "Original content not found in %s" path) :error))))) |
| 236 | + (spit path (:new-full-content result)) |
| 237 | + (handle-file-change-result result path (format "Successfully replaced content in %s." path))) |
| 238 | + (handle-file-change-result result path nil))))) |
234 | 239 |
|
235 | 240 | (defn ^:private edit-file [arguments components] |
236 | | - (change-file arguments components false)) |
| 241 | + (change-file arguments components)) |
237 | 242 |
|
238 | | -(defn ^:private plan-edit-file [arguments components] |
239 | | - (change-file arguments components true)) |
| 243 | +(defn ^:private preview-file-change [arguments {:keys [db]}] |
| 244 | + (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))]]) |
| 245 | + (let [path (get arguments "path") |
| 246 | + original-content (get arguments "original_content") |
| 247 | + new-content (get arguments "new_content") |
| 248 | + all? (boolean (get arguments "all_occurrences")) |
| 249 | + file-exists? (fs/exists? path)] |
| 250 | + (cond |
| 251 | + file-exists? |
| 252 | + (let [result (text-match/apply-content-change-to-file path original-content new-content all?)] |
| 253 | + (handle-file-change-result result path |
| 254 | + (format "Change simulation completed for %s. Original file unchanged - preview only." path))) |
| 255 | + |
| 256 | + (and (not file-exists?) (= "" original-content)) |
| 257 | + (tools.util/single-text-content (format "New file creation simulation completed for %s. File will be created - preview only." path)) |
| 258 | + |
| 259 | + :else |
| 260 | + (tools.util/single-text-content |
| 261 | + (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." |
| 262 | + path) |
| 263 | + :error))))) |
240 | 264 |
|
241 | 265 | (defn ^:private move-file [arguments {:keys [db]}] |
242 | 266 | (let [workspace-dirs (tools.util/workspace-roots-strs db)] |
|
284 | 308 | :required ["path" "content"]} |
285 | 309 | :handler #'write-file |
286 | 310 | ;; TODO - add behaviors to config and define disabled tools there! |
287 | | - :enabled-fn (fn [{:keys [behavior]}] (not= "plan" behavior)) |
| 311 | + :enabled-fn #(not= "plan" (:behavior %)) |
288 | 312 | :summary-fn #'write-file-summary} |
289 | 313 | "eca_edit_file" |
290 | 314 | {:description (tools.util/read-tool-description "eca_edit_file") |
|
299 | 323 | :description "Whether to replace all occurences of the file or just the first one (default)"}} |
300 | 324 | :required ["path" "original_content" "new_content"]} |
301 | 325 | :handler #'edit-file |
302 | | - :enabled-fn (fn [{:keys [behavior]}] (not= "plan" behavior)) |
303 | | - :summary-fn (constantly "Editing file")} |
304 | | - "eca_plan_edit_file" |
305 | | - {:description (str "Plan a file change where user needs to apply or reject the change. " |
306 | | - "Replace a specific string or content block in a file with new content. " |
307 | | - "Finds the exact original content and replaces it with new content. " |
308 | | - "Be extra careful to format the original-content exactly correctly, " |
309 | | - "taking extra care with whitespace and newlines. " |
310 | | - "Avoid replacing whole functions, methods, or classes, change only the needed code. " |
311 | | - "In addition to replacing strings, this can also be used to prepend, append, or delete contents from a file.") |
312 | | - :parameters {:type "object" |
313 | | - :properties {"path" {:type "string" |
314 | | - :description "The absolute file path to do the replace."} |
315 | | - "original_content" {:type "string" |
316 | | - :description "The exact content to find and replace"} |
317 | | - "new_content" {:type "string" |
318 | | - :description "The new content to replace the original content with"} |
319 | | - "all_occurrences" {:type "boolean" |
320 | | - :description "Whether to replace all occurences of the file or just the first one (default)"}} |
321 | | - :required ["path" "original_content" "new_content"]} |
322 | | - :handler #'plan-edit-file |
323 | | - ;; TODO improve plan behavior providing better tool for exit plan and present to user. |
324 | | - :enabled-fn (constantly false) #_(fn [{:keys [behavior]}] (= "plan" behavior)) |
325 | | - :summary-fn (constantly "Planning edit")} |
| 326 | + :enabled-fn #(not= "plan" (:behavior %)) |
| 327 | + :summary-fn (constantly "Editting file")} |
| 328 | + "eca_preview_file_change" |
| 329 | + {:description (tools.util/read-tool-description "eca_preview_file_change") |
| 330 | + :parameters {:type "object" |
| 331 | + :properties {"path" {:type "string" |
| 332 | + :description "The absolute file path to preview changes for."} |
| 333 | + "original_content" {:type "string" |
| 334 | + :description "The exact content to find in the file"} |
| 335 | + "new_content" {:type "string" |
| 336 | + :description "The content to show as replacement in the preview"} |
| 337 | + "all_occurrences" {:type "boolean" |
| 338 | + :description "Whether to preview replacing all occurrences or just the first one (default)"}} |
| 339 | + :required ["path" "original_content" "new_content"]} |
| 340 | + :handler #'preview-file-change |
| 341 | + :enabled-fn #(= "plan" (:behavior %)) |
| 342 | + :summary-fn (constantly "Previewing change")} |
326 | 343 | "eca_move_file" |
327 | 344 | {:description (tools.util/read-tool-description "eca_move_file") |
328 | 345 | :parameters {:type "object" |
|
332 | 349 | :description "The new absolute file path to move to."}} |
333 | 350 | :required ["source" "destination"]} |
334 | 351 | :handler #'move-file |
335 | | - :enabled-fn (fn [{:keys [behavior]}] (not= "plan" behavior)) |
| 352 | + :enabled-fn #(not= "plan" (:behavior %)) |
336 | 353 | :summary-fn (constantly "Moving file")} |
337 | 354 | "eca_grep" |
338 | 355 | {:description (tools.util/read-tool-description "eca_grep") |
|
353 | 370 | (let [path (get arguments "path") |
354 | 371 | original-content (get arguments "original_content") |
355 | 372 | new-content (get arguments "new_content") |
356 | | - all? (get arguments "all_occurrences")] |
357 | | - (when-let [{:keys [original-full-content |
358 | | - new-full-content]} (and path (fs/exists? path) original-content new-content |
359 | | - (file-change-full-content path original-content new-content all?))] |
360 | | - (let [{:keys [added removed diff]} (diff/diff original-full-content new-full-content path)] |
| 373 | + all? (get arguments "all_occurrences") |
| 374 | + file-exists? (and path (fs/exists? path))] |
| 375 | + (cond |
| 376 | + (and file-exists? original-content new-content) |
| 377 | + (let [result (text-match/apply-content-change-to-file path original-content new-content all?) |
| 378 | + original-full-content (:original-full-content result)] |
| 379 | + (when original-full-content |
| 380 | + (if-let [new-full-content (:new-full-content result)] |
| 381 | + (let [{:keys [added removed diff]} (diff/diff original-full-content new-full-content path)] |
| 382 | + {:type :fileChange |
| 383 | + :path path |
| 384 | + :linesAdded added |
| 385 | + :linesRemoved removed |
| 386 | + :diff diff}) |
| 387 | + (logger/warn "tool-call-details-before-invocation - NO DIFF GENERATED because match failed for path:" path)))) |
| 388 | + |
| 389 | + (and (not file-exists?) (= original-content "") new-content path) |
| 390 | + (let [{:keys [added removed diff]} (diff/diff "" new-content path)] |
361 | 391 | {:type :fileChange |
362 | 392 | :path path |
363 | 393 | :linesAdded added |
364 | 394 | :linesRemoved removed |
365 | | - :diff diff})))) |
| 395 | + :diff diff}) |
| 396 | + |
| 397 | + :else nil))) |
366 | 398 |
|
367 | | -(defmethod tools.util/tool-call-details-before-invocation :eca_plan_edit_file [name arguments] |
368 | | - (tools.util/tool-call-details-before-invocation :eca_edit_file name arguments)) |
| 399 | +(defmethod tools.util/tool-call-details-before-invocation :eca_preview_file_change [_name arguments] |
| 400 | + (tools.util/tool-call-details-before-invocation :eca_edit_file arguments)) |
369 | 401 |
|
370 | 402 | (defmethod tools.util/tool-call-details-before-invocation :eca_write_file [_name arguments] |
371 | 403 | (let [path (get arguments "path") |
|
0 commit comments