summaryrefslogtreecommitdiff
path: root/gptel-tools/list_directory_files.el
blob: 8da9ba28d03989fb42e623c5fff67ee40d509065 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
;;; list_directory_files.el --- List directory files for GPTel -*- coding: utf-8; lexical-binding: t; -*-

;; Copyright (C) 2025

;; Author: gptel-tool-writer
;; Keywords: convenience, tools
;; Version: 2.0.0

;; This file is not part of GNU Emacs.

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;;; Commentary:

;; This tool provides a comprehensive directory listing function for use within gptel.
;; It lists files and directories with detailed attributes such as size, last modification time,
;; permissions, and executable status, supporting optional recursive traversal and filtering
;; by file extension.
;;
;; Features:
;; - Lists files with Unix-style permissions, size, and modification date
;; - Optional recursive directory traversal
;; - Filter files by extension
;; - Graceful error handling and reporting
;; - Human-readable file sizes and dates

;;; Code:

(require 'cl-lib)
(require 'seq)
(require 'subr-x)

;;; Helper Functions

(defun list-directory-files--mode-to-permissions (mode)
  "Convert numeric MODE to symbolic Unix style permissions string."
  (concat
   (if (eq (logand #o40000 mode) #o40000) "d" "-")
   (mapconcat
    (lambda (bit)
      (cond ((eq bit ?r) (if (> (logand mode #o400) 0) "r" "-"))
            ((eq bit ?w) (if (> (logand mode #o200) 0) "w" "-"))
            ((eq bit ?x) (if (> (logand mode #o100) 0) "x" "-"))))
    '(?r ?w ?x) "")
   (mapconcat
    (lambda (bit)
      (cond ((eq bit ?r) (if (> (logand mode #o040) 0) "r" "-"))
            ((eq bit ?w) (if (> (logand mode #o020) 0) "w" "-"))
            ((eq bit ?x) (if (> (logand mode #o010) 0) "x" "-"))))
    '(?r ?w ?x) "")
   (mapconcat
    (lambda (bit)
      (cond ((eq bit ?r) (if (> (logand mode #o004) 0) "r" "-"))
            ((eq bit ?w) (if (> (logand mode #o002) 0) "w" "-"))
            ((eq bit ?x) (if (> (logand mode #o001) 0) "x" "-"))))
    '(?r ?w ?x) "")))

(defun list-directory-files--get-file-info (filepath)
  "Get file information for FILEPATH as a plist."
  (condition-case err
      (let* ((attrs (file-attributes filepath 'string))
             (size (file-attribute-size attrs))
             (last-mod (file-attribute-modification-time attrs))
             (dirp (eq t (file-attribute-type attrs)))
             (mode (file-modes filepath))
             (perm (list-directory-files--mode-to-permissions mode))
             (execp (file-executable-p filepath)))
        (list :success t
              :path filepath
              :size size
              :last-modified last-mod
              :is-directory dirp
              :permissions perm
              :executable execp))
    (error
     (list :success nil
           :path filepath
           :error (error-message-string err)))))

(defun list-directory-files--filter-by-extension (extension)
  "Create a filter function for files with EXTENSION."
  (when extension
    (lambda (file-info)
      (or (plist-get file-info :is-directory)  ; Always include directories
          (and (plist-get file-info :success)
               (string-suffix-p (concat "." extension)
                              (file-name-nondirectory (plist-get file-info :path))
                              t))))))

(defun list-directory-files--format-file-entry (file-info base-path)
  "Format a single FILE-INFO entry relative to BASE-PATH."
  (format "  %s%s %10s %s %s"
          (plist-get file-info :permissions)
          (if (plist-get file-info :executable) "*" " ")
          (file-size-human-readable (or (plist-get file-info :size) 0))
          (format-time-string "%Y-%m-%d %H:%M" (plist-get file-info :last-modified))
          (file-relative-name (plist-get file-info :path) base-path)))

;;; Core Implementation

(defun list-directory-files--list-directory (path &optional recursive filter max-depth current-depth)
  "List files in PATH directory.
RECURSIVE enables subdirectory traversal.
FILTER is a predicate function for filtering files.
MAX-DEPTH limits recursion depth (nil for unlimited).
CURRENT-DEPTH tracks the current recursion level."
  (let ((files '())
        (errors '())
        (current-depth (or current-depth 0))
        (expanded-path (expand-file-name (or path ".") "~")))
    
    (if (not (file-directory-p expanded-path))
        ;; Return error if not a directory
        (list :files nil 
              :errors (list (format "Not a directory: %s" expanded-path)))
      ;; Process directory
      (condition-case err
          (dolist (entry (directory-files expanded-path t "^\\([^.]\\|\\.[^.]\\|\\.\\..\\)"))
            (let ((info (list-directory-files--get-file-info entry)))
              (if (plist-get info :success)
                  (progn
                    ;; Add file if it passes the filter
                    (when (or (not filter) (funcall filter info))
                      (push info files))
                    ;; Recurse into directories if needed
                    (when (and recursive
                             (plist-get info :is-directory)
                             (or (not max-depth) (< current-depth max-depth)))
                      (let ((subdir-result (list-directory-files--list-directory
                                          entry recursive filter max-depth (1+ current-depth))))
                        (setq files (nconc files (plist-get subdir-result :files)))
                        (setq errors (nconc errors (plist-get subdir-result :errors))))))
                ;; Handle file access error
                (push (format "%s: %s" (plist-get info :path) (plist-get info :error)) errors))))
        (error
         (push (format "Error accessing directory %s: %s" expanded-path (error-message-string err)) errors)))
      
      (list :files (nreverse files) :errors (nreverse errors)))))

(defun list-directory-files--format-output (path result)
  "Format the directory listing RESULT for PATH as a string."
  (let ((files (plist-get result :files))
        (errors (plist-get result :errors))
        (base-path (expand-file-name "~")))
    (concat
     (when files
       (format "Found %d file%s in %s:\n%s"
               (length files)
               (if (= (length files) 1) "" "s")
               path
               (mapconcat (lambda (f) (list-directory-files--format-file-entry f base-path))
                        files "\n")))
     (when (and files errors) "\n\n")
     (when errors
       (format "Errors encountered:\n%s"
               (mapconcat (lambda (e) (format "  - %s" e)) errors "\n")))
     ;; Handle case where there are no files and no errors
     (when (and (not files) (not errors))
       (format "No files found in %s" path)))))

;;; Tool Registration

(gptel-make-tool
 :name "list_directory_files"
 :function (lambda (path &optional recursive filter-extension)
             "List files in directory PATH.
RECURSIVE enables subdirectory listing.
FILTER-EXTENSION limits results to files with the specified extension."
             (let* ((filter (list-directory-files--filter-by-extension filter-extension))
                    (result (list-directory-files--list-directory path recursive filter)))
               (list-directory-files--format-output (or path ".") result)))
 :description "List files in a directory with detailed attributes. Returns formatted listing with permissions, size, modification time."
 :args (list '(:name "path"
                    :type string
                    :description "Directory path to list (relative to home directory)")
            '(:name "recursive"
                    :type boolean
                    :description "Recursively list subdirectories"
                    :optional t)
            '(:name "filter-extension"
                    :type string
                    :description "Only include files with this extension"
                    :optional t))
 :category "filesystem"
 :confirm nil
 :include t)

;; Automatically add to gptel-tools on load
(add-to-list 'gptel-tools (gptel-get-tool '("filesystem" "list_directory_files")))

(provide 'list_directory_files)
;;; list_directory_files.el ends here