@@ -116,13 +116,14 @@ called with the tool's spec as their argument."
116116;;;### autoload
117117(defun llm-tool-collection-get-category (category )
118118 " Return a list of all tool definitions in the collection part of CATEGORY.
119+ CATEGORY should be a symbol like \\= 'filesystem or \\= 'buffers.
119120
120121Mapping over this list with `gptel-make-tool' , `llm-make-tool' , or
121122similar will add all tools to the respective client:
122123
123124 (mapcar (apply-partially #\\= 'apply #\\= 'gptel-make-tool)
124- (llm-tool-collection-get-category \" filesystem \" ))"
125- (seq-filter (lambda (tool ) (string= (plist-get tool :category ) category))
125+ (llm-tool-collection-get-category \\= 'filesystem ))"
126+ (seq-filter (lambda (tool ) (eq (plist-get tool :category ) category))
126127 (llm-tool-collection-get-all)))
127128
128129;;;### autoload
@@ -147,7 +148,7 @@ similar will add all tools to the respective client:
147148(llm-tool-collection-deftool read-file
148149 (:description
149150 " Read the contents of a file and return its content as a string."
150- :category " filesystem"
151+ :category filesystem
151152 :confirm t
152153 :include t )
153154 ((path
@@ -159,7 +160,7 @@ similar will add all tools to the respective client:
159160
160161(llm-tool-collection-deftool list-directory
161162 (:description " List the contents of a specified directory."
162- :category " filesystem"
163+ :category filesystem
163164 :confirm t
164165 :include t )
165166 ((path
@@ -168,14 +169,16 @@ similar will add all tools to the respective client:
168169 (let ((expanded-path (expand-file-name path)))
169170 (if (file-directory-p expanded-path)
170171 (string-join `(,(format " Contents of %s : " path)
171- ,@(directory-files expanded-path))
172+ ,@(seq-filter (lambda (file )
173+ (not (member file '(" ." " .." ))))
174+ (directory-files expanded-path)))
172175 " \n " )
173176 (error " %s is not a directory" expanded-path))))
174177
175178(llm-tool-collection-deftool create-file
176179 (:description
177180 " Create a new file with the specified content if it does not already exist."
178- :category " filesystem"
181+ :category filesystem
179182 :confirm t )
180183 ((path
181184 :type string
@@ -191,7 +194,7 @@ similar will add all tools to the respective client:
191194(llm-tool-collection-deftool create-directory
192195 (:description
193196 " Create a new directory at the specified path if it does not already exist."
194- :category " filesystem"
197+ :category filesystem
195198 :confirm t )
196199 ((path
197200 :type string
@@ -206,7 +209,7 @@ similar will add all tools to the respective client:
206209 (:description " View contents of BUFFER-NAME with optional OFFSET and LIMIT.
207210OFFSET specifies the starting line (0-based).
208211LIMIT specifies the maximum number of lines to return."
209- :category " buffers" )
212+ :category buffers)
210213 ((buffer-name :type string :description " Name of the buffer to view." )
211214 &optional
212215 (offset
@@ -221,6 +224,279 @@ LIMIT specifies the maximum number of lines to return."
221224 (selected-lines (seq-subseq lines start end)))
222225 (string-join selected-lines " \n " ))))
223226
227+ (llm-tool-collection-deftool bash
228+ (:description " Executes bash commands"
229+ :category system
230+ :confirm t )
231+ ((command
232+ :type string :description " Command to execute" )
233+ &optional
234+ (timeout
235+ :type number :description " Optional timeout in milliseconds (max 600000)" ))
236+ ; ; TODO: use the timeout
237+ (let ((result (shell-command-to-string command)))
238+ (if (string-empty-p result)
239+ " Command executed successfully (no output)"
240+ result)))
241+
242+ (llm-tool-collection-deftool glob
243+ (:description " File pattern matching"
244+ :category filesystem
245+ :include t )
246+ ((pattern
247+ :type string
248+ :description " Glob pattern to match files" )
249+ &optional
250+ (path
251+ :type string
252+ :description " Directory to search in" ))
253+ (let* ((default-directory (or path default-directory))
254+ (files (file-expand-wildcards pattern)))
255+ (string-join files " \n " )))
256+
257+ (llm-tool-collection-deftool grep
258+ (:description " Content search using regex"
259+ :category search
260+ :include t )
261+ ((pattern
262+ :type string
263+ :description " Regex pattern to search in file contents" )
264+ &optional
265+ (include
266+ :type string
267+ :description " File pattern to include in search" )
268+ (path
269+ :type string
270+ :description " Directory to search in" ))
271+ (let* ((default-directory (or path default-directory))
272+ (include-arg (if include
273+ (format " --include=\" %s \" " include)
274+ " " ))
275+ (command (format " grep -r -n -E %s %s . "
276+ (shell-quote-argument pattern)
277+ include-arg))
278+ (result (shell-command-to-string command)))
279+ (if (string-empty-p result)
280+ " No matches found"
281+ result)))
282+
283+ (llm-tool-collection-deftool ls
284+ (:description " Lists files and directories"
285+ :category filesystem
286+ :include t )
287+ ((path
288+ :type string
289+ :description " Absolute path to directory to list" )
290+ &optional
291+ (ignore
292+ :type array :items (:type string)
293+ :description " Array of Elisp regexp patterns (e.g., \\ .pdf$) to ignore" ))
294+ (let ((files (directory-files path t nil t )))
295+ (when ignore
296+ (let ((ignore-patterns (if (stringp ignore)
297+ (list ignore)
298+ ignore)))
299+ (seq-do (lambda (pattern )
300+ (setq files (seq-filter (lambda (f )
301+ (not (string-match-p pattern f)))
302+ files )))
303+ ignore-patterns)))
304+ (string-join (mapcar #'file-name-nondirectory files ) " \n " )))
305+
306+ (llm-tool-collection-deftool view-file
307+ (:description " Reads files"
308+ :category filesystem
309+ :include t )
310+ ((file-path
311+ :type string
312+ :description " Absolute path to the file to read" )
313+ &optional
314+ (offset
315+ :type number
316+ :description " Line number to start reading from" )
317+ (limit
318+ :type number
319+ :description " Number of lines to read" ))
320+ (with-temp-buffer
321+ (insert-file-contents file-path)
322+ (let* ((lines (split-string (buffer-string ) " \n " ))
323+ (start (or offset 0 ))
324+ (end (if limit (min (+ start limit) (length lines)) (length lines)))
325+ (selected-lines (seq-subseq lines start end)))
326+ (string-join selected-lines " \n " ))))
327+
328+ (llm-tool-collection-deftool edit-file
329+ (:description " Edits files"
330+ :category filesystem
331+ :confirm t )
332+ ((file-path
333+ :type string
334+ :description " Absolute path to the file to modify" )
335+ (old-string
336+ :type string
337+ :description " Text to replace (must match exactly)" )
338+ (new-string
339+ :type string
340+ :description " Text to replace old_string with" ))
341+ (with-temp-buffer
342+ (insert-file-contents file-path)
343+ (let ((case-fold-search nil ))
344+ (if (string= old-string " " )
345+ (progn
346+ (erase-buffer )
347+ (insert new-string)
348+ (write-file file-path)
349+ (format " Created new file: %s " file-path))
350+ (goto-char (point-min ))
351+ (let ((count 0 ))
352+ (while (search-forward old-string nil t )
353+ (setq count (1+ count)))
354+ (if (= count 0 )
355+ (format " Error: Could not find text to replace in %s " file-path)
356+ (if (> count 1 )
357+ (format " Error: Found %d matches for the text to replace in %s " count file-path)
358+ (goto-char (point-min ))
359+ (search-forward old-string)
360+ (replace-match new-string t t )
361+ (write-file file-path)
362+ (format " Successfully edited %s " file-path))))))))
363+
364+ (llm-tool-collection-deftool replace-file
365+ (:description " Completely overwrites files"
366+ :category filesystem
367+ :confirm t )
368+ ((file-path
369+ :type string
370+ :description " Absolute path to file to write" )
371+ (content
372+ :type string
373+ :description " Content to write to the file" ))
374+ (with-temp-buffer
375+ (insert content)
376+ (write-file file-path)
377+ (format " File replaced: %s " file-path)))
378+
379+ ; ; Buffer-related tools
380+
381+ (llm-tool-collection-deftool buffer-search
382+ (:description " Searches within Emacs buffers for text patterns"
383+ :category buffers
384+ :include t )
385+ ((pattern
386+ :type string
387+ :description " Regex pattern to search for in buffer contents.\n Regex syntax is that of Emacs -- parentheses are NOT escaped! Search for \" '(defun\" , not \" \\\\ (defun\" ." )
388+ &optional
389+ (buffer-name
390+ :type string
391+ :description " Buffer name to search in (searches all user buffers if not specified)" ))
392+ (condition-case err
393+ (let ((results '()))
394+ (dolist (buf (if buffer-name
395+ (if-let ((specified-buf (get-buffer buffer-name)))
396+ (list specified-buf)
397+ (error " Buffer '%s ' does not exist " buffer-name))
398+ (seq-filter (lambda (buf )
399+ (let ((buf-name (buffer-name buf)))
400+ (and (not (string-prefix-p " " buf-name))
401+ (not (string-prefix-p " *" buf-name)))))
402+ (buffer-list ))))
403+ (with-current-buffer buf
404+ (save-excursion
405+ (goto-char (point-min ))
406+ (let ((line-num 1 ))
407+ (while (re-search-forward pattern nil t )
408+ (let* ((line-beg (line-beginning-position ))
409+ (line-end (line-end-position ))
410+ (line-text (buffer-substring-no-properties line-beg line-end))
411+ (col-num (- (point ) line-beg)))
412+ (push (format " %s :%d :%d : %s "
413+ (buffer-name buf)
414+ line-num
415+ col-num
416+ line-text)
417+ results))
418+ (forward-line 1 )
419+ (setq line-num (1+ line-num)))))))
420+ (if results
421+ (string-join (nreverse results) " \n " )
422+ " No matches found" ))
423+ (invalid-regexp
424+ (format " Invalid regexp pattern: %s " (error-message-string err)))
425+ (error
426+ (format " Error during search: %s " (error-message-string err)))))
427+
428+ (llm-tool-collection-deftool list-buffers
429+ (:description " Lists active buffers that a user might care about"
430+ :category buffers
431+ :include t )
432+ ()
433+ (let* ((all-buffers (buffer-list ))
434+ (user-buffers (seq-filter
435+ (lambda (buf )
436+ (let ((buf-name (buffer-name buf)))
437+ (and (not (string-prefix-p " " buf-name))
438+ (not (string-prefix-p " *" buf-name))
439+ (not (eq buf (current-buffer ))))))
440+ all-buffers))
441+ (sorted-buffers (sort user-buffers
442+ (lambda (a b )
443+ (string< (buffer-name a)
444+ (buffer-name b))))))
445+ (mapconcat (lambda (buf )
446+ (let ((buf-name (buffer-name buf))
447+ (file-name (or (buffer-file-name buf) " " )))
448+ (format " %s%s "
449+ buf-name
450+ (if (string-empty-p file-name)
451+ " "
452+ (format " (%s ) " file-name)))))
453+ sorted-buffers
454+ " \n " )))
455+
456+ (llm-tool-collection-deftool edit-buffer
457+ (:description " Edits Emacs buffers"
458+ :category buffers
459+ :confirm t )
460+ ((buffer-name
461+ :type string
462+ :description " Name of the buffer to modify" )
463+ (old-string
464+ :type string
465+ :description " Text to replace (must match exactly)" )
466+ (new-string
467+ :type string
468+ :description " Text to replace old_string with" ))
469+ (with-current-buffer buffer-name
470+ (let ((case-fold-search nil ))
471+ (save-excursion
472+ (goto-char (point-min ))
473+ (let ((count 0 ))
474+ (while (search-forward old-string nil t )
475+ (setq count (1+ count)))
476+ (if (= count 0 )
477+ (format " Error: Could not find text to replace in buffer %s " buffer-name)
478+ (if (> count 1 )
479+ (format " Error: Found %d matches for the text to replace in buffer %s " count buffer-name)
480+ (goto-char (point-min ))
481+ (search-forward old-string)
482+ (replace-match new-string t t )
483+ (format " Successfully edited buffer %s " buffer-name))))))))
484+
485+ (llm-tool-collection-deftool replace-buffer
486+ (:description " Completely overwrites buffer contents"
487+ :category buffers
488+ :confirm t )
489+ ((buffer-name
490+ :type string
491+ :description " Name of the buffer to overwrite" )
492+ (content
493+ :type string
494+ :description " Content to write to the buffer" ))
495+ (with-current-buffer buffer-name
496+ (erase-buffer )
497+ (insert content)
498+ (format " Buffer replaced: %s " buffer-name)))
499+
224500(provide 'llm-tool-collection )
225501
226502; ;; llm-tool-collection.el ends here
0 commit comments