commit 6cb91f228a2f0433a7bc93205df8a5f3bd3b2304
parent d57c422cddf657fab565411ffe9956a32ff37230
Author: Vineet Kumar <git@vineetk.net>
Date: Sun, 24 May 2026 13:50:36 -0400
rename from gptel-tools an add gptel-agent's bash tool
Diffstat:
| D | gptel-tools-hl.el | | | 102 | ------------------------------------------------------------------------------- |
| D | gptel-tools.el | | | 61 | ------------------------------------------------------------- |
| A | llm-tools-hl.el | | | 107 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | llm-tools.el | | | 122 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4 files changed, 229 insertions(+), 163 deletions(-)
diff --git a/gptel-tools-hl.el b/gptel-tools-hl.el
@@ -1,102 +0,0 @@
-(defvar hl-bigrams
- (list "aa" "ab" "ac" "ad" "ae" "af" "ag" "ah" "ai" "aj" "ak" "al" "am" "an"
- "ao" "ap" "aq" "ar" "as" "at" "au" "av" "aw" "ax" "ay" "az" "ba" "bb"
- "bc" "bd" "be" "bf" "bg" "bh" "bi" "bj" "bk" "bl" "bm" "bn" "bo" "bp"
- "br" "bs" "bt" "bu" "bv" "bw" "bx" "by" "bz" "ca" "cb" "cc" "cd" "ce"
- "cf" "cg" "ch" "ci" "cj" "ck" "cl" "cm" "cn" "co" "cp" "cq" "cr" "cs"
- "ct" "cu" "cv" "cw" "cx" "cy" "cz" "da" "db" "dc" "dd" "de" "df" "dg"
- "dh" "di" "dj" "dk" "dl" "dm" "dn" "do" "dp" "dq" "dr" "ds" "dt" "du"
- "dv" "dw" "dx" "dy" "dz" "ea" "eb" "ec" "ed" "ee" "ef" "eg" "eh" "ei"
- "ej" "ek" "el" "em" "en" "eo" "ep" "eq" "er" "es" "et" "eu" "ev" "ew"
- "ex" "ey" "ez" "fa" "fb" "fc" "fd" "fe" "ff" "fg" "fh" "fi" "fj" "fk"
- "fl" "fm" "fn" "fo" "fp" "fq" "fr" "fs" "ft" "fu" "fv" "fw" "fx" "fy"
- "fz" "ga" "gb" "gc" "gd" "ge" "gf" "gg" "gh" "gi" "gj" "gl" "gm" "gn"
- "go" "gp" "gr" "gs" "gt" "gu" "gv" "gw" "gx" "gy" "gz" "ha" "hb" "hc"
- "hd" "he" "hf" "hg" "hh" "hi" "hj" "hk" "hl" "hm" "hn" "ho" "hp" "hq"
- "hr" "hs" "ht" "hu" "hv" "hw" "hx" "hy" "hz" "ia" "ib" "ic" "id" "ie"
- "if" "ig" "ih" "ii" "ij" "ik" "il" "im" "in" "io" "ip" "iq" "ir" "is"
- "it" "iu" "iv" "iw" "ix" "iy" "iz" "ja" "jb" "jc" "jd" "je" "jf" "jg"
- "jh" "ji" "jj" "jk" "jl" "jm" "jn" "jo" "jp" "jq" "jr" "js" "jt" "ju"
- "jw" "jx" "jy" "ka" "kb" "kc" "kd" "ke" "kf" "kg" "kh" "ki" "kj" "kk"
- "kl" "km" "kn" "ko" "kp" "kr" "ks" "kt" "ku" "kv" "kw" "kx" "ky" "la"
- "lb" "lc" "ld" "le" "lf" "lg" "lh" "li" "lj" "lk" "ll" "lm" "ln" "lo"
- "lp" "lr" "ls" "lt" "lu" "lv" "lw" "lx" "ly" "lz" "ma" "mb" "mc" "md"
- "me" "mf" "mg" "mh" "mi" "mj" "mk" "ml" "mm" "mn" "mo" "mp" "mq" "mr"
- "ms" "mt" "mu" "mv" "mw" "mx" "my" "mz" "na" "nb" "nc" "nd" "ne" "nf"
- "ng" "nh" "ni" "nj" "nk" "nl" "nm" "nn" "no" "np" "nr" "ns" "nt" "nu"
- "nv" "nw" "nx" "ny" "nz" "oa" "ob" "oc" "od" "oe" "of" "og" "oh" "oi"
- "oj" "ok" "ol" "om" "on" "oo" "op" "oq" "or" "os" "ot" "ou" "ov" "ow"
- "ox" "oy" "oz" "pa" "pb" "pc" "pd" "pe" "pf" "pg" "ph" "pi" "pj" "pk"
- "pl" "pm" "pn" "po" "pp" "pq" "pr" "ps" "pt" "pu" "pv" "pw" "px" "py"
- "pz" "qa" "qb" "qc" "qd" "qe" "qh" "qi" "ql" "qm" "qn" "qo" "qp" "qq"
- "qr" "qs" "qt" "qu" "qw" "qx" "qy" "ra" "rb" "rc" "rd" "re" "rf" "rg"
- "rh" "ri" "rk" "rl" "rm" "rn" "ro" "rp" "rq" "rr" "rs" "rt" "ru" "rv"
- "rw" "rx" "ry" "rz" "sa" "sb" "sc" "sd" "se" "sf" "sg" "sh" "si" "sj"
- "sk" "sl" "sm" "sn" "so" "sp" "sq" "sr" "ss" "st" "su" "sv" "sw" "sx"
- "sy" "sz" "ta" "tb" "tc" "td" "te" "tf" "tg" "th" "ti" "tj" "tk" "tl"
- "tm" "tn" "to" "tp" "tr" "ts" "tt" "tu" "tv" "tw" "tx" "ty" "tz" "ua"
- "ub" "uc" "ud" "ue" "uf" "ug" "uh" "ui" "uj" "uk" "ul" "um" "un" "uo"
- "up" "uq" "ur" "us" "ut" "uu" "uv" "uw" "ux" "uy" "uz" "va" "vb" "vc"
- "vd" "ve" "vf" "vg" "vh" "vi" "vj" "vk" "vl" "vm" "vn" "vo" "vp" "vq"
- "vr" "vs" "vt" "vu" "vv" "vw" "vx" "vy" "vz" "wa" "wb" "wc" "wd" "we"
- "wf" "wg" "wh" "wi" "wj" "wk" "wl" "wm" "wn" "wo" "wp" "wr" "ws" "wt"
- "wu" "wv" "ww" "wx" "wy" "xa" "xb" "xc" "xd" "xe" "xf" "xh" "xi" "xl"
- "xm" "xn" "xo" "xp" "xr" "xs" "xt" "xu" "xx" "xy" "xz" "ya" "yb" "yc"
- "yd" "ye" "yf" "yg" "yh" "yi" "yj" "yk" "yl" "ym" "yn" "yo" "yp" "yr"
- "ys" "yt" "yu" "yv" "yw" "yx" "yy" "yz" "za" "zb" "zc" "zd" "ze" "zf"
- "zg" "zh" "zi" "zk" "zl" "zm" "zn" "zo" "zp" "zr" "zs" "zt" "zu" "zw"
- "zx" "zy" "zz")
- "List of bigrams for use with hashline reads/writes.
-
-Each of the bigrams resolve to a single token in standard LLM
-vocabulary, unlike a hash's hex digits.
-
-Taken from https://github.com/can1357/oh-my-pi.
-Precisely: https://raw.githubusercontent.com/can1357/oh-my-pi/85003ca/packages/coding-agent/src/hashline/bigrams.json")
-
-(defun hl-hash (line)
- (elt hl-bigrams (% (sxhash line) 647)))
-
-(defun hl-file-lines (file &optional beg end)
- "Return FILE contents as a list of strings.
-With optional BEG and END (inclusive, 1-based), return the lines between that range."
- (with-temp-buffer
- (insert-file-contents-literally file)
- (let ((lines (string-lines (buffer-string))))
- (if beg
- (cl-subseq lines (1- beg) end)
- lines))))
-
-(defun hl-format-lines (lines &optional start include-content?)
- "Format LINES in hashline format.
-START is 1-based (default 1).
-If INCLUDE-CONTENT is non-nil, append `|LINE' after each hash."
- (string-join
- (cl-mapcar (lambda (line i)
- (if include-content?
- (format "%d%2s|%s" i (hl-hash line) line)
- (format "%d%2s" i (hl-hash line))))
- lines
- (number-sequence (or start 1) (+ (or start 1) (1- (length lines)))))
- "\n"))
-
-(defun hl-file-read (file)
- "Return FILE contents in hashline format."
- (hl-format-lines (hl-file-lines file) nil t))
-
-(defun hl-file-read-range (file beg end)
- "Return FILE contents in hashline format from lines BEG to END."
- (hl-format-lines (hl-file-lines file beg end) beg t))
-
-;; FIXME might be redundant with =hl-file-line-hash= if I'm only
-;; verifying the hash of the anchors and telling LLM to re-read the
-;; file when they don't match
-(defun hl-file-read-hashes (file)
- "Return FILE contents in hashline format without content."
- (hl-format-lines (hl-file-lines file)))
-
-(defun hl-file-line-hash (file n)
- "Return the hash of the Nth line in FILE."
- (let ((lines (hl-file-lines file)))
- (hl-hash (elt lines (1- n)))))
-
diff --git a/gptel-tools.el b/gptel-tools.el
@@ -1,61 +0,0 @@
-;; read_file (hl-file-read)
-(gptel-make-tool
- :name "read_file"
- :function 'hl-file-read
- :description "\
-Read an entire file and return its contents in hashline format. Each
-line is represented as LINE_NUMBER HASH|CONTENT, where LINE_NUMBER is
-the 1-based line number, HASH is a 2-character content-derived bigram,
-and CONTENT is the verbatim line text. Example output:
-
-1aa|const char *TITLE = \"Mr\";
-2ab|void greet(char *name) {
-3cd| return name;
-
-The LINE_NUMBER and HASH together form an anchor (e.g., 1aa, 2ab) that
-uniquely identifies a line by position and content. Copy anchors
-verbatim from the output when referencing lines in edit operations. Do
-not fabricate or modify hashes. Call this tool before editing any file
-to obtain current content and valid anchors. Re-call if anchors no
-longer match after external changes."
- :args (list '(:name "file"
- :type string
- :description "Path to the file to read, relative to the working directory."))
- :async nil
- :confirm nil
- :include t)
-
-;; TODO write_file (with-temp-buffer + insert)
-;; TODO edit_file (hashline + patch apply)
-
-;; TODO bash_readonly (shell-command-to-string, whitelisted commands)
-;; TODO bash (shell-command-to-string, needs heavy filtering)
-
-;; find_files (directory-files-recursively)
-(gptel-make-tool
- :name "find_files"
- :function (lambda (dir &optional pattern)
- (let ((git-dir (concat (file-name-as-directory dir) ".git")))
- (delq nil (mapcar (lambda (f)
- (unless (string-prefix-p git-dir f) f))
- (directory-files-recursively dir (or pattern ""))))))
- :description "\
-Recursively find all files in a given directory, excluding files within
-.git directories. Returns a list of absolute file paths. Optionally
-accepts a pattern to filter file names (e.g., `*.el' for Emacs Lisp
-files). Use this to discover files in a project directory before reading
-or editing them."
- :args (list '(:name "directory"
- :type string
- :description "Path to the directory to be read, relative to the working directory.")
- '(:name "pattern"
- :type string
- :description "Optional file name pattern to filter results, e.g. `*.el' or `*.txt'."
- :optional t))
- :async nil
- :confirm nil
- :include t)
-
-;; TODO grep (just grep :D)
-;; TODO fetch_url (use gptel-agent's one as a base, but needs mime handling like PDFs and images)
-;; TODO web_search (use gptel-agent's eww/shr method as a base)
diff --git a/llm-tools-hl.el b/llm-tools-hl.el
@@ -0,0 +1,107 @@
+(defvar llm-tools--hl-bigrams
+ (list "aa" "ab" "ac" "ad" "ae" "af" "ag" "ah" "ai" "aj" "ak" "al" "am" "an"
+ "ao" "ap" "aq" "ar" "as" "at" "au" "av" "aw" "ax" "ay" "az" "ba" "bb"
+ "bc" "bd" "be" "bf" "bg" "bh" "bi" "bj" "bk" "bl" "bm" "bn" "bo" "bp"
+ "br" "bs" "bt" "bu" "bv" "bw" "bx" "by" "bz" "ca" "cb" "cc" "cd" "ce"
+ "cf" "cg" "ch" "ci" "cj" "ck" "cl" "cm" "cn" "co" "cp" "cq" "cr" "cs"
+ "ct" "cu" "cv" "cw" "cx" "cy" "cz" "da" "db" "dc" "dd" "de" "df" "dg"
+ "dh" "di" "dj" "dk" "dl" "dm" "dn" "do" "dp" "dq" "dr" "ds" "dt" "du"
+ "dv" "dw" "dx" "dy" "dz" "ea" "eb" "ec" "ed" "ee" "ef" "eg" "eh" "ei"
+ "ej" "ek" "el" "em" "en" "eo" "ep" "eq" "er" "es" "et" "eu" "ev" "ew"
+ "ex" "ey" "ez" "fa" "fb" "fc" "fd" "fe" "ff" "fg" "fh" "fi" "fj" "fk"
+ "fl" "fm" "fn" "fo" "fp" "fq" "fr" "fs" "ft" "fu" "fv" "fw" "fx" "fy"
+ "fz" "ga" "gb" "gc" "gd" "ge" "gf" "gg" "gh" "gi" "gj" "gl" "gm" "gn"
+ "go" "gp" "gr" "gs" "gt" "gu" "gv" "gw" "gx" "gy" "gz" "ha" "hb" "hc"
+ "hd" "he" "hf" "hg" "hh" "hi" "hj" "hk" "hl" "hm" "hn" "ho" "hp" "hq"
+ "hr" "hs" "ht" "hu" "hv" "hw" "hx" "hy" "hz" "ia" "ib" "ic" "id" "ie"
+ "if" "ig" "ih" "ii" "ij" "ik" "il" "im" "in" "io" "ip" "iq" "ir" "is"
+ "it" "iu" "iv" "iw" "ix" "iy" "iz" "ja" "jb" "jc" "jd" "je" "jf" "jg"
+ "jh" "ji" "jj" "jk" "jl" "jm" "jn" "jo" "jp" "jq" "jr" "js" "jt" "ju"
+ "jw" "jx" "jy" "ka" "kb" "kc" "kd" "ke" "kf" "kg" "kh" "ki" "kj" "kk"
+ "kl" "km" "kn" "ko" "kp" "kr" "ks" "kt" "ku" "kv" "kw" "kx" "ky" "la"
+ "lb" "lc" "ld" "le" "lf" "lg" "lh" "li" "lj" "lk" "ll" "lm" "ln" "lo"
+ "lp" "lr" "ls" "lt" "lu" "lv" "lw" "lx" "ly" "lz" "ma" "mb" "mc" "md"
+ "me" "mf" "mg" "mh" "mi" "mj" "mk" "ml" "mm" "mn" "mo" "mp" "mq" "mr"
+ "ms" "mt" "mu" "mv" "mw" "mx" "my" "mz" "na" "nb" "nc" "nd" "ne" "nf"
+ "ng" "nh" "ni" "nj" "nk" "nl" "nm" "nn" "no" "np" "nr" "ns" "nt" "nu"
+ "nv" "nw" "nx" "ny" "nz" "oa" "ob" "oc" "od" "oe" "of" "og" "oh" "oi"
+ "oj" "ok" "ol" "om" "on" "oo" "op" "oq" "or" "os" "ot" "ou" "ov" "ow"
+ "ox" "oy" "oz" "pa" "pb" "pc" "pd" "pe" "pf" "pg" "ph" "pi" "pj" "pk"
+ "pl" "pm" "pn" "po" "pp" "pq" "pr" "ps" "pt" "pu" "pv" "pw" "px" "py"
+ "pz" "qa" "qb" "qc" "qd" "qe" "qh" "qi" "ql" "qm" "qn" "qo" "qp" "qq"
+ "qr" "qs" "qt" "qu" "qw" "qx" "qy" "ra" "rb" "rc" "rd" "re" "rf" "rg"
+ "rh" "ri" "rk" "rl" "rm" "rn" "ro" "rp" "rq" "rr" "rs" "rt" "ru" "rv"
+ "rw" "rx" "ry" "rz" "sa" "sb" "sc" "sd" "se" "sf" "sg" "sh" "si" "sj"
+ "sk" "sl" "sm" "sn" "so" "sp" "sq" "sr" "ss" "st" "su" "sv" "sw" "sx"
+ "sy" "sz" "ta" "tb" "tc" "td" "te" "tf" "tg" "th" "ti" "tj" "tk" "tl"
+ "tm" "tn" "to" "tp" "tr" "ts" "tt" "tu" "tv" "tw" "tx" "ty" "tz" "ua"
+ "ub" "uc" "ud" "ue" "uf" "ug" "uh" "ui" "uj" "uk" "ul" "um" "un" "uo"
+ "up" "uq" "ur" "us" "ut" "uu" "uv" "uw" "ux" "uy" "uz" "va" "vb" "vc"
+ "vd" "ve" "vf" "vg" "vh" "vi" "vj" "vk" "vl" "vm" "vn" "vo" "vp" "vq"
+ "vr" "vs" "vt" "vu" "vv" "vw" "vx" "vy" "vz" "wa" "wb" "wc" "wd" "we"
+ "wf" "wg" "wh" "wi" "wj" "wk" "wl" "wm" "wn" "wo" "wp" "wr" "ws" "wt"
+ "wu" "wv" "ww" "wx" "wy" "xa" "xb" "xc" "xd" "xe" "xf" "xh" "xi" "xl"
+ "xm" "xn" "xo" "xp" "xr" "xs" "xt" "xu" "xx" "xy" "xz" "ya" "yb" "yc"
+ "yd" "ye" "yf" "yg" "yh" "yi" "yj" "yk" "yl" "ym" "yn" "yo" "yp" "yr"
+ "ys" "yt" "yu" "yv" "yw" "yx" "yy" "yz" "za" "zb" "zc" "zd" "ze" "zf"
+ "zg" "zh" "zi" "zk" "zl" "zm" "zn" "zo" "zp" "zr" "zs" "zt" "zu" "zw"
+ "zx" "zy" "zz")
+ "List of bigrams for use with hashline reads/writes.
+
+Each of the bigrams resolve to a single token in standard LLM
+vocabulary, unlike a hash's hex digits.
+
+Taken from https://github.com/can1357/oh-my-pi.
+Precisely: https://raw.githubusercontent.com/can1357/oh-my-pi/85003ca/packages/coding-agent/src/hashline/bigrams.json")
+
+(defun llm-tools--hl-hash (line)
+ (elt llm-tools--hl-bigrams (% (sxhash line) 647)))
+
+(defun llm-tools--hl-file-lines (file &optional beg end)
+ "Return FILE contents as a list of strings.
+With optional BEG and END (inclusive, 1-based), return the lines between that range."
+ (with-temp-buffer
+ (insert-file-contents-literally file)
+ (let ((lines (string-lines (buffer-string))))
+ (if beg
+ (cl-subseq lines (1- beg) end)
+ lines))))
+
+(defun llm-tools--hl-format-lines (lines &optional start include-content?)
+ "Format LINES in hashline format.
+START is 1-based (default 1).
+If INCLUDE-CONTENT is non-nil, append `|LINE' after each hash."
+ (string-join
+ (cl-mapcar (lambda (line i)
+ (if include-content?
+ (format "%d%2s|%s" i (llm-tools--hl-hash line) line)
+ (format "%d%2s" i (llm-tools--hl-hash line))))
+ lines
+ (number-sequence (or start 1) (+ (or start 1) (1- (length lines)))))
+ "\n"))
+
+(defun llm-tools--hl-file-read (file)
+ "Return FILE contents in hashline format."
+ (llm-tools--hl-format-lines (llm-tools--hl-file-lines file) nil t))
+
+(defun llm-tools--hl-file-read-range (file beg end)
+ "Return FILE contents in hashline format from lines BEG to END."
+ (llm-tools--hl-format-lines (llm-tools--hl-file-lines file beg end) beg t))
+
+;; FIXME might be redundant with =llm-tools--hl-file-line-hash= if I'm only
+;; verifying the hash of the anchors and telling LLM to re-read the
+;; file when they don't match
+(defun llm-tools--hl-file-read-hashes (file)
+ "Return FILE contents in hashline format without content."
+ (llm-tools--hl-format-lines (llm-tools--hl-file-lines file)))
+
+(defun llm-tools--hl-file-line-hash (file n)
+ "Return the hash of the Nth line in FILE."
+ (let ((lines (llm-tools--hl-file-lines file)))
+ (llm-tools--hl-hash (elt lines (1- n)))))
+
+;; before applying patch, first read the file to see if there were any changes
+;; if the llm's replacement text's hashlines do not match current hashlines, reject the request
+;; 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?
+;; 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
+;; so I just need to verify if the anchors didn't change
diff --git a/llm-tools.el b/llm-tools.el
@@ -0,0 +1,122 @@
+;; read_file (hl-file-read)
+(gptel-make-tool
+ :name "read_file"
+ :function 'llm-tools--hl-file-read
+ :description "\
+Read an entire file and return its contents in hashline format. Each
+line is represented as LINE_NUMBER HASH|CONTENT, where LINE_NUMBER is
+the 1-based line number, HASH is a 2-character content-derived bigram,
+and CONTENT is the verbatim line text. Example output:
+
+1aa|const char *TITLE = \"Mr\";
+2ab|void greet(char *name) {
+3cd| return name;
+
+The LINE_NUMBER and HASH together form an anchor (e.g., 1aa, 2ab) that
+uniquely identifies a line by position and content. Copy anchors
+verbatim from the output when referencing lines in edit operations. Do
+not fabricate or modify hashes. Call this tool before editing any file
+to obtain current content and valid anchors. Re-call if anchors no
+longer match after external changes."
+ :args '((:name "file"
+ :type string
+ :description "Path to the file to read, relative to the working directory."))
+ :async nil
+ :confirm nil
+ :include t)
+
+;; TODO write_file (with-temp-buffer + insert)
+;; TODO edit_file (hashline + patch apply)
+
+;; TODO bash_readonly (shell-command-to-string, whitelisted commands)
+;; TODO compile command (calls build system, doesn't need confirmation, whitelisted commands)
+
+;; bash (make-process, taken from gptel-agent)
+(defun llm-tools--execute-bash (callback command)
+ "Execute COMMAND asynchronously in bash and call CALLBACK with output.
+
+CALLBACK is called with the command output string when the process finishes.
+COMMAND is the bash command string to execute."
+ (let* ((output-buffer (generate-new-buffer " *llm-tools-bash*"))
+ (proc (make-process
+ :name "llm-tools-bash"
+ :buffer output-buffer
+ :command (list "bash" "-c" command)
+ :connection-type 'pipe
+ :sentinel
+ (lambda (process _event)
+ (when (memq (process-status process) '(exit signal))
+ (let* ((exit-code (process-exit-status process))
+ (output (with-current-buffer (process-buffer process)
+ (buffer-string))))
+ (kill-buffer (process-buffer process))
+ (funcall callback
+ (if (zerop exit-code)
+ output
+ (format "Command failed with exit code %d:\nSTDOUT+STDERR:\n%s"
+ exit-code output)))))))))
+ proc))
+
+(gptel-make-tool
+ :name "Bash"
+ :function #'llm-tools--execute-bash
+ :description "Execute Bash commands.
+
+This tool provides access to a Bash shell with GNU coreutils (or
+equivalents) available. Use this to inspect system state, run builds,
+tests or other development or system administration tasks.
+
+Do NOT use this for file operations, finding, reading or editing files.
+Use the provided file tools instead: `read_file`, `write_file`,
+`edit_file`, `find_file`, `grep`.
+
+- Quote file paths with spaces using double quotes.
+- Chain dependent commands with && (or ; if failures are OK)
+- Use absolute paths instead of cd when possible
+- For parallel commands, make multiple `Bash` calls in one message
+- Run tests, check your work or otherwise close the loop to verify changes you make.
+
+EXAMPLES:
+- Find recent errors: 'grep -i error /var/log/app.log | tail -20'
+- Check file type: 'file document.pdf'
+- Count lines: 'wc -l *.txt'
+
+The command will be executed in the current working directory. Output is
+returned as a string. Long outputs should be filtered/limited using pipes."
+ :args '(( :name "command"
+ :type string
+ :description "The Bash command to execute. \
+Can include pipes and standard shell operators.
+Example: 'ls -la | head -20' or 'grep -i error app.log | tail -50'"))
+ :confirm t
+ :include t
+ :async t)
+
+;; find_files (directory-files-recursively)
+(gptel-make-tool
+ :name "find_files"
+ :function (lambda (dir &optional pattern)
+ (let ((git-dir (concat (file-name-as-directory dir) ".git")))
+ (delq nil (mapcar (lambda (f)
+ (unless (string-prefix-p git-dir f) f))
+ (directory-files-recursively dir (or pattern ""))))))
+ :description "\
+Recursively find all files in a given directory, excluding files within
+.git directories. Returns a list of absolute file paths. Optionally
+accepts a pattern to filter file names (e.g., `*.el' for Emacs Lisp
+files). Use this to discover files in a project directory before reading
+or editing them."
+ :args '((:name "directory"
+ :type string
+ :description "Path to the directory to be read, relative to the working directory.")
+ (:name "pattern"
+ :type string
+ :description "Optional file name pattern to filter results, e.g. `*.el' or `*.txt'."
+ :optional t))
+ :async nil
+ :confirm nil
+ :include t)
+
+;; TODO grep (just grep :D)
+;; TODO fetch_url (use gptel-agent's one as a base, but needs mime handling like PDFs and images)
+;; TODO web_search (use gptel-agent's eww/shr method as a base)