diff options
Diffstat (limited to 'gptel-tools/list_directory_files.el')
| -rw-r--r-- | gptel-tools/list_directory_files.el | 200 |
1 files changed, 200 insertions, 0 deletions
diff --git a/gptel-tools/list_directory_files.el b/gptel-tools/list_directory_files.el new file mode 100644 index 00000000..8da9ba28 --- /dev/null +++ b/gptel-tools/list_directory_files.el @@ -0,0 +1,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
\ No newline at end of file |
