llm-tools.el

Opinionated tools for use by a (local) LLM
Log | Files | Refs | LICENSE

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:
Dgptel-tools-hl.el | 102-------------------------------------------------------------------------------
Dgptel-tools.el | 61-------------------------------------------------------------
Allm-tools-hl.el | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Allm-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)