summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVineet Kumar <git@vineetk.net>2026-05-24 13:50:36 -0400
committerVineet Kumar <git@vineetk.net>2026-05-24 14:10:39 -0400
commit6cb91f228a2f0433a7bc93205df8a5f3bd3b2304 (patch)
tree19c3fa40a70b8663ea4401f88051eac684e3a500
parentd57c422cddf657fab565411ffe9956a32ff37230 (diff)
rename from gptel-tools an add gptel-agent's bash tool
-rw-r--r--gptel-tools.el61
-rw-r--r--llm-tools-hl.el (renamed from gptel-tools-hl.el)39
-rw-r--r--llm-tools.el122
3 files changed, 144 insertions, 78 deletions
diff --git a/gptel-tools.el b/gptel-tools.el
deleted file mode 100644
index 1e8453c..0000000
--- a/gptel-tools.el
+++ /dev/null
@@ -1,61 +0,0 @@
1;; read_file (hl-file-read)
2(gptel-make-tool
3 :name "read_file"
4 :function 'hl-file-read
5 :description "\
6Read an entire file and return its contents in hashline format. Each
7line is represented as LINE_NUMBER HASH|CONTENT, where LINE_NUMBER is
8the 1-based line number, HASH is a 2-character content-derived bigram,
9and CONTENT is the verbatim line text. Example output:
10
111aa|const char *TITLE = \"Mr\";
122ab|void greet(char *name) {
133cd| return name;
14
15The LINE_NUMBER and HASH together form an anchor (e.g., 1aa, 2ab) that
16uniquely identifies a line by position and content. Copy anchors
17verbatim from the output when referencing lines in edit operations. Do
18not fabricate or modify hashes. Call this tool before editing any file
19to obtain current content and valid anchors. Re-call if anchors no
20longer match after external changes."
21 :args (list '(:name "file"
22 :type string
23 :description "Path to the file to read, relative to the working directory."))
24 :async nil
25 :confirm nil
26 :include t)
27
28;; TODO write_file (with-temp-buffer + insert)
29;; TODO edit_file (hashline + patch apply)
30
31;; TODO bash_readonly (shell-command-to-string, whitelisted commands)
32;; TODO bash (shell-command-to-string, needs heavy filtering)
33
34;; find_files (directory-files-recursively)
35(gptel-make-tool
36 :name "find_files"
37 :function (lambda (dir &optional pattern)
38 (let ((git-dir (concat (file-name-as-directory dir) ".git")))
39 (delq nil (mapcar (lambda (f)
40 (unless (string-prefix-p git-dir f) f))
41 (directory-files-recursively dir (or pattern ""))))))
42 :description "\
43Recursively find all files in a given directory, excluding files within
44.git directories. Returns a list of absolute file paths. Optionally
45accepts a pattern to filter file names (e.g., `*.el' for Emacs Lisp
46files). Use this to discover files in a project directory before reading
47or editing them."
48 :args (list '(:name "directory"
49 :type string
50 :description "Path to the directory to be read, relative to the working directory.")
51 '(:name "pattern"
52 :type string
53 :description "Optional file name pattern to filter results, e.g. `*.el' or `*.txt'."
54 :optional t))
55 :async nil
56 :confirm nil
57 :include t)
58
59;; TODO grep (just grep :D)
60;; TODO fetch_url (use gptel-agent's one as a base, but needs mime handling like PDFs and images)
61;; TODO web_search (use gptel-agent's eww/shr method as a base)
diff --git a/gptel-tools-hl.el b/llm-tools-hl.el
index fd08260..c682334 100644
--- a/gptel-tools-hl.el
+++ b/llm-tools-hl.el
@@ -1,4 +1,4 @@
1(defvar hl-bigrams 1(defvar llm-tools--hl-bigrams
2 (list "aa" "ab" "ac" "ad" "ae" "af" "ag" "ah" "ai" "aj" "ak" "al" "am" "an" 2 (list "aa" "ab" "ac" "ad" "ae" "af" "ag" "ah" "ai" "aj" "ak" "al" "am" "an"
3 "ao" "ap" "aq" "ar" "as" "at" "au" "av" "aw" "ax" "ay" "az" "ba" "bb" 3 "ao" "ap" "aq" "ar" "as" "at" "au" "av" "aw" "ax" "ay" "az" "ba" "bb"
4 "bc" "bd" "be" "bf" "bg" "bh" "bi" "bj" "bk" "bl" "bm" "bn" "bo" "bp" 4 "bc" "bd" "be" "bf" "bg" "bh" "bi" "bj" "bk" "bl" "bm" "bn" "bo" "bp"
@@ -54,10 +54,10 @@ vocabulary, unlike a hash's hex digits.
54Taken from https://github.com/can1357/oh-my-pi. 54Taken from https://github.com/can1357/oh-my-pi.
55Precisely: https://raw.githubusercontent.com/can1357/oh-my-pi/85003ca/packages/coding-agent/src/hashline/bigrams.json") 55Precisely: https://raw.githubusercontent.com/can1357/oh-my-pi/85003ca/packages/coding-agent/src/hashline/bigrams.json")
56 56
57(defun hl-hash (line) 57(defun llm-tools--hl-hash (line)
58 (elt hl-bigrams (% (sxhash line) 647))) 58 (elt llm-tools--hl-bigrams (% (sxhash line) 647)))
59 59
60(defun hl-file-lines (file &optional beg end) 60(defun llm-tools--hl-file-lines (file &optional beg end)
61 "Return FILE contents as a list of strings. 61 "Return FILE contents as a list of strings.
62With optional BEG and END (inclusive, 1-based), return the lines between that range." 62With optional BEG and END (inclusive, 1-based), return the lines between that range."
63 (with-temp-buffer 63 (with-temp-buffer
@@ -67,36 +67,41 @@ With optional BEG and END (inclusive, 1-based), return the lines between that ra
67 (cl-subseq lines (1- beg) end) 67 (cl-subseq lines (1- beg) end)
68 lines)))) 68 lines))))
69 69
70(defun hl-format-lines (lines &optional start include-content?) 70(defun llm-tools--hl-format-lines (lines &optional start include-content?)
71 "Format LINES in hashline format. 71 "Format LINES in hashline format.
72START is 1-based (default 1). 72START is 1-based (default 1).
73If INCLUDE-CONTENT is non-nil, append `|LINE' after each hash." 73If INCLUDE-CONTENT is non-nil, append `|LINE' after each hash."
74 (string-join 74 (string-join
75 (cl-mapcar (lambda (line i) 75 (cl-mapcar (lambda (line i)
76 (if include-content? 76 (if include-content?
77 (format "%d%2s|%s" i (hl-hash line) line) 77 (format "%d%2s|%s" i (llm-tools--hl-hash line) line)
78 (format "%d%2s" i (hl-hash line)))) 78 (format "%d%2s" i (llm-tools--hl-hash line))))
79 lines 79 lines
80 (number-sequence (or start 1) (+ (or start 1) (1- (length lines))))) 80 (number-sequence (or start 1) (+ (or start 1) (1- (length lines)))))
81 "\n")) 81 "\n"))
82 82
83(defun hl-file-read (file) 83(defun llm-tools--hl-file-read (file)
84 "Return FILE contents in hashline format." 84 "Return FILE contents in hashline format."
85 (hl-format-lines (hl-file-lines file) nil t)) 85 (llm-tools--hl-format-lines (llm-tools--hl-file-lines file) nil t))
86 86
87(defun hl-file-read-range (file beg end) 87(defun llm-tools--hl-file-read-range (file beg end)
88 "Return FILE contents in hashline format from lines BEG to END." 88 "Return FILE contents in hashline format from lines BEG to END."
89 (hl-format-lines (hl-file-lines file beg end) beg t)) 89 (llm-tools--hl-format-lines (llm-tools--hl-file-lines file beg end) beg t))
90 90
91;; FIXME might be redundant with =hl-file-line-hash= if I'm only 91;; FIXME might be redundant with =llm-tools--hl-file-line-hash= if I'm only
92;; verifying the hash of the anchors and telling LLM to re-read the 92;; verifying the hash of the anchors and telling LLM to re-read the
93;; file when they don't match 93;; file when they don't match
94(defun hl-file-read-hashes (file) 94(defun llm-tools--hl-file-read-hashes (file)
95 "Return FILE contents in hashline format without content." 95 "Return FILE contents in hashline format without content."
96 (hl-format-lines (hl-file-lines file))) 96 (llm-tools--hl-format-lines (llm-tools--hl-file-lines file)))
97 97
98(defun hl-file-line-hash (file n) 98(defun llm-tools--hl-file-line-hash (file n)
99 "Return the hash of the Nth line in FILE." 99 "Return the hash of the Nth line in FILE."
100 (let ((lines (hl-file-lines file))) 100 (let ((lines (llm-tools--hl-file-lines file)))
101 (hl-hash (elt lines (1- n))))) 101 (llm-tools--hl-hash (elt lines (1- n)))))
102 102
103;; before applying patch, first read the file to see if there were any changes
104;; if the llm's replacement text's hashlines do not match current hashlines, reject the request
105;; maybe if it just shifted a little bit (since the anchor is line number and hash of the line's content), there can be some kind of autohealing?
106;; rather, the llm would not format it like the reads. it's just going to provide text according to what the anchors it gives say
107;; so I just need to verify if the anchors didn't change
diff --git a/llm-tools.el b/llm-tools.el
new file mode 100644
index 0000000..6476d15
--- /dev/null
+++ b/llm-tools.el
@@ -0,0 +1,122 @@
1;; read_file (hl-file-read)
2(gptel-make-tool
3 :name "read_file"
4 :function 'llm-tools--hl-file-read
5 :description "\
6Read an entire file and return its contents in hashline format. Each
7line is represented as LINE_NUMBER HASH|CONTENT, where LINE_NUMBER is
8the 1-based line number, HASH is a 2-character content-derived bigram,
9and CONTENT is the verbatim line text. Example output:
10
111aa|const char *TITLE = \"Mr\";
122ab|void greet(char *name) {
133cd| return name;
14
15The LINE_NUMBER and HASH together form an anchor (e.g., 1aa, 2ab) that
16uniquely identifies a line by position and content. Copy anchors
17verbatim from the output when referencing lines in edit operations. Do
18not fabricate or modify hashes. Call this tool before editing any file
19to obtain current content and valid anchors. Re-call if anchors no
20longer match after external changes."
21 :args '((:name "file"
22 :type string
23 :description "Path to the file to read, relative to the working directory."))
24 :async nil
25 :confirm nil
26 :include t)
27
28;; TODO write_file (with-temp-buffer + insert)
29;; TODO edit_file (hashline + patch apply)
30
31;; TODO bash_readonly (shell-command-to-string, whitelisted commands)
32;; TODO compile command (calls build system, doesn't need confirmation, whitelisted commands)
33
34;; bash (make-process, taken from gptel-agent)
35(defun llm-tools--execute-bash (callback command)
36 "Execute COMMAND asynchronously in bash and call CALLBACK with output.
37
38CALLBACK is called with the command output string when the process finishes.
39COMMAND is the bash command string to execute."
40 (let* ((output-buffer (generate-new-buffer " *llm-tools-bash*"))
41 (proc (make-process
42 :name "llm-tools-bash"
43 :buffer output-buffer
44 :command (list "bash" "-c" command)
45 :connection-type 'pipe
46 :sentinel
47 (lambda (process _event)
48 (when (memq (process-status process) '(exit signal))
49 (let* ((exit-code (process-exit-status process))
50 (output (with-current-buffer (process-buffer process)
51 (buffer-string))))
52 (kill-buffer (process-buffer process))
53 (funcall callback
54 (if (zerop exit-code)
55 output
56 (format "Command failed with exit code %d:\nSTDOUT+STDERR:\n%s"
57 exit-code output)))))))))
58 proc))
59
60(gptel-make-tool
61 :name "Bash"
62 :function #'llm-tools--execute-bash
63 :description "Execute Bash commands.
64
65This tool provides access to a Bash shell with GNU coreutils (or
66equivalents) available. Use this to inspect system state, run builds,
67tests or other development or system administration tasks.
68
69Do NOT use this for file operations, finding, reading or editing files.
70Use the provided file tools instead: `read_file`, `write_file`,
71`edit_file`, `find_file`, `grep`.
72
73- Quote file paths with spaces using double quotes.
74- Chain dependent commands with && (or ; if failures are OK)
75- Use absolute paths instead of cd when possible
76- For parallel commands, make multiple `Bash` calls in one message
77- Run tests, check your work or otherwise close the loop to verify changes you make.
78
79EXAMPLES:
80- Find recent errors: 'grep -i error /var/log/app.log | tail -20'
81- Check file type: 'file document.pdf'
82- Count lines: 'wc -l *.txt'
83
84The command will be executed in the current working directory. Output is
85returned as a string. Long outputs should be filtered/limited using pipes."
86 :args '(( :name "command"
87 :type string
88 :description "The Bash command to execute. \
89Can include pipes and standard shell operators.
90Example: 'ls -la | head -20' or 'grep -i error app.log | tail -50'"))
91 :confirm t
92 :include t
93 :async t)
94
95;; find_files (directory-files-recursively)
96(gptel-make-tool
97 :name "find_files"
98 :function (lambda (dir &optional pattern)
99 (let ((git-dir (concat (file-name-as-directory dir) ".git")))
100 (delq nil (mapcar (lambda (f)
101 (unless (string-prefix-p git-dir f) f))
102 (directory-files-recursively dir (or pattern ""))))))
103 :description "\
104Recursively find all files in a given directory, excluding files within
105.git directories. Returns a list of absolute file paths. Optionally
106accepts a pattern to filter file names (e.g., `*.el' for Emacs Lisp
107files). Use this to discover files in a project directory before reading
108or editing them."
109 :args '((:name "directory"
110 :type string
111 :description "Path to the directory to be read, relative to the working directory.")
112 (:name "pattern"
113 :type string
114 :description "Optional file name pattern to filter results, e.g. `*.el' or `*.txt'."
115 :optional t))
116 :async nil
117 :confirm nil
118 :include t)
119
120;; TODO grep (just grep :D)
121;; TODO fetch_url (use gptel-agent's one as a base, but needs mime handling like PDFs and images)
122;; TODO web_search (use gptel-agent's eww/shr method as a base)