;;; gptel-org-tools.el --- LLM Tools for org-mode interaction.  -*- lexical-binding: t; -*-

;; Copyright (C) 2025  Phil Bajsicki

;; Author: Phil Bajsicki <phil@bajsicki.com>
;; Keywords: extensions, comm, tools, matching, convenience,
;;
;; Author: Phil Bajsicki <phil@bajsicki.com>
;; Version: 0.0.1
;; Package-Requires: ((emacs "30.1") (gptel 0.9.8) (org-ql 0.9))
;; URL: https://github.com/phil/gptel-org-tools
;; SPDX-License-Identifier: GPL-3.0

;; 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, version 3 of the License.

;; 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.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;; This file is NOT part of GNU Emacs.

;; This file is not part of GNU Emacs.

;;; Commentary:

;;  All documentation regarding these functions is in the README.org file.
;;  Repository: https://git.bajsicki.com/phil/gptel-org-tools

;;; Code:

(gptel-make-tool
 :function (lambda (arg)
	     (with-temp-buffer
	       (ibuffer)
	       (let ((content (buffer-string)))
	  (kill-buffer (current-buffer))
	  content)))
 :name "list-buffers"
 :description "Access the list of buffers open in Emacs, including file names and full paths."
 :args (list '(:name "arg"
	       :type string
	       :description "Does nothing."
	       :optional t))
 :category "emacs")

(gptel-make-tool
 :function (lambda (dir)
	     (with-temp-buffer
	       (dired (or dir "~"))
	       (let ((content (buffer-string)))
		 (kill-buffer (current-buffer))
		 content)))
 :name "dired"
 :description "List directory contents"
 :args (list '(:name "dir"
               :type string
               :description "Directory path"
	       :optional t))
 :category "filesystem")

(gptel-make-tool
 :function (lambda (filename)
	     (bufferp (find-buffer-visiting (expand-file-name filename))))
 :name "find-buffer-visiting"
 :description "Check if the file is open in a buffer. Usage (find-buffer-visiting filename)"
 :args (list '(:name "filename"
	       :type string
	       :description "The filename to compare to open buffers."))
 :category "org-mode")

(gptel-make-tool
 :function (lambda (file)
      (find-file-noselect file))
 :name "find-file-noselect"
 :description "Open the file in a buffer. This doesn't interfere with the user."
 :args (list '(:name "file"
	:type string
	:description "Path to file.."))
 :category "filesystem")

(defun gptel-org-tools-org-extract-tags (buffer)
  (interactive "bBuffer: ")
  (with-current-buffer buffer
    (let ((tags '()))
      (org-map-entries
       (lambda ()
         (let* ((components (org-heading-components))
                (tag-string (car (last components))))
           (when tag-string
             (dolist (tag (split-string tag-string ":" t))
               (push tag tags))))))
      (sort (-uniq tags) #'string<))))

(gptel-make-tool
 :function #'gptel-org-tools-org-extract-tags
 :name "org-extract-tags"
 :description "Extract all unique tags from an org-mode buffer"
 :args (list '(:name "buffer"
               :type string
               :description "The Org buffer to extract tags from."))
 :category "org-mode")

(defun gptel-org-tools-org-extract-headings (buffer)
  (interactive "bBuffer: ")
  (with-current-buffer buffer
    (org-map-entries
     #'(buffer-substring-no-properties
	(line-beginning-position)
	(line-end-position))
     t
     'file)))

(gptel-make-tool
 :function #'gptel-org-tools-org-extract-headings
 :name "org-extract-headings"
 :description "Extract all headings from an org-mode buffer"
 :args (list '(:name "buffer"
               :type string
               :description "The Org buffer to extract headings from."))
 :category "org-mode")

(defun gptel-org-tools-org-ql-select (buf query)
  (org-ql-select
    (get-buffer buf)
    (if (stringp query)
	(read query)
      query)
    :action #'(lambda ()
		(concat
		 (buffer-substring-no-properties
		  (line-beginning-position)
		  (progn
		  (outline-next-heading)
		  (line-beginning-position)))))))

(gptel-make-tool
 :function #'gptel-org-tools-org-ql-select
 :name "org-ql-select"
 :description "Run org-ql-select against buffer with query. Using filename fails."
 :args (list '(:name "buffer"
               :type string
               :description "The name of the buffer. Can be multiple buffers. See the NAME column in `emacs-list-buffers`.")
             '(:name "query"
               :type string
               :description "The query to pass into org-ql-select. See org-ql documentation for syntax. Usually `(tags \"tag1\" \"tag2\")` is sufficient. Possible predicates: `tags` (finds both local and inherited tags), `tags-local` (finds only local tags), `rifle` (matches against both heading and body text). This is a sexp, not a string."))
 :category "org")

(defun gptel-org-tools-org-ql-select-dates (buf date)
  (interactive "fBuffer: \nsDate (YYYY or YYYY-MM): ")
  (org-ql-select
    (get-buffer buf)
    `(heading ,date)
    :action #'(lambda ()
		(buffer-substring-no-properties
		 (line-beginning-position)
		 (org-end-of-subtree)))))


(gptel-make-tool
 :function #'gptel-org-tools-org-ql-select-dates
 :name "org-ql-select-dates"
 :description "Extract org subtree by date in YYYY or YYYY-MM format"
 :args (list '(:name "buffer"
               :type string
               :description "Buffer name.")
             '(:name "date"
               :type string
               :description "Date in YYYY or YYYY-MM format. Can add multiple like so: \"YYYY-MM\" \"YYYY-MM\""))
 :category "org")

(gptel-make-tool
 :function (lambda (days)
             (with-temp-buffer
	       (org-agenda-list (or days 14))
	       (let ((content (buffer-string)))
	  (kill-buffer (current-buffer))
	  content)))
 :name "org-agenda-fortnight"
 :description "Get the next 14 days of user's org-agenda."
 :args (list '(:name "days"
	       :type integer
	       :description "The number of days to look ahead. Default: 14"))
 :category "org")

(defun gptel-org-tools-org-ql-select-headings (buf query)
  (org-ql-select
    (get-buffer buf)
    `(heading ,query)
    :action #'(lambda ()
		(concat
		 (buffer-substring-no-properties
		  (line-beginning-position)
		  (line-end-position))))))


(gptel-make-tool
 :function #'gptel-org-tools-org-ql-select-headings
 :name "org-ql-select-headings"
 :description "Retreive matching headings from buffer using org-ql-select. Matches only against heading. Using filename fails."
 :args (list '(:name "buffer"
               :type string
               :description "The name of the buffer. See the NAME column in ~emacs-list-buffers~.")
             '(:name "query"
               :type string
               :description "The string to pass into org-ql-select-headings. This is a bare string. Example: \"searchterm\""))
 :category "org-ql")

(defun gptel-org-tools-org-ql-select-headings-rifle (buf query)
	     (org-ql-select
	       (get-buffer buf)
	       `(rifle ,query)
	       :action :action #'(lambda ()
		    (concat
		      (buffer-substring-no-properties
			(line-beginning-position)
			(line-end-position))))))


 (gptel-make-tool
  :function #'gptel-org-tools-org-ql-select-headings-rifle
  :name "org-ql-select-headings-rifle"
  :description "Retreive headings from buffer using org-ql-select. Matches against both heading and content. Using filename fails."
  :args (list '(:name "buffer"
                :type string
                :description "The name of the buffer. See the NAME column in ~emacs-list-buffers~.")
              '(:name "query"
                :type string
                :description "The string to pass into org-ql-select-headings-rifle. This is a bare string. Example: \"searchterm\""))
  :category "org-ql")

(defun gptel-org-tools-org-ql-select-tags-local (buf query)
  (org-ql-select
    (get-buffer buf)
    `(tags-local ,query)
    :action #'(lambda ()
		(concat
		 (buffer-substring-no-properties
		  (line-beginning-position)
		  (progn
		  (outline-next-heading)
		  (line-beginning-position)))))))


(gptel-make-tool
 :function #'gptel-org-tools-org-ql-select-tags-local
 :name "org-ql-select-tags-local"
 :description "Run org-ql-select-tags-local against buffer with query. No tag inheritance."
 :args (list '(:name "buffer"
               :type string
               :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.")
             '(:name "query"
               :type string
               :description "The tags to match entry headings against. Example: \"tag1\" \"tag2\""))
 :category "org-ql")

(defun gptel-org-tools-org-ql-select-tags (buf query)
  (org-ql-select
    (get-buffer buf)
    `(tags ,query)
    :action #'(lambda ()
		(concat
		 (buffer-substring-no-properties
		  (line-beginning-position)
		  (progn
		  (outline-next-heading)
		  (line-beginning-position)))))))


(gptel-make-tool
 :function #'gptel-org-tools-org-ql-select-tags
 :name "org-ql-select-tags"
 :description "Run org-ql-select-tags against buffer with query. Supports tag inheritance."
 :args (list '(:name "buffer"
               :type string
               :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.")
             '(:name "query"
               :type string
               :description "The tags to match entry headings against. Example: \"tag1\" \"tag2\""))
 :category "org-ql")

(defun gptel-org-tools-org-ql-select-rifle (buf query)
  (org-ql-select
    (get-buffer buf)
    `(rifle ,query)
    :action #'(lambda ()
		(concat
		 (buffer-substring-no-properties
		  (line-beginning-position)
		  (progn
		  (outline-next-heading)
		  (line-beginning-position)))))))

(gptel-make-tool
 :function #'gptel-org-tools-org-ql-select-rifle
 :name "org-ql-select-rifle"
 :description "Run org-ql-select-rifle against buffer with query."
 :args (list '(:name "buffer"
               :type string
               :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.")
             '(:name "query"
               :type string
               :description "The strings to match entry headings and content against. Example: \"tag1\" \"tag2\""))
 :category "org-ql")

(provide 'gptel-org-tools)
;;; gptel-org-tools.el ends here