625 lines
27 KiB
EmacsLisp
625 lines
27 KiB
EmacsLisp
;;; init-ai.el --- Configuración de inteligencias artificales -*- lexical-binding: t -*-
|
|
|
|
;; Author: kj <webmaster@outcontrol.net>
|
|
;; URL: https://git.kj2.me/kj/confi-emacs-actual
|
|
|
|
;;; Commentary:
|
|
|
|
;; Configuración para Inteligencia artifical en Emacs.
|
|
|
|
;;; Code:
|
|
|
|
;; Cliente LLM (ollama, chatgpt, gemini, etc.)
|
|
(use-package gptel
|
|
:defer nil
|
|
:config
|
|
(when (getenv "GEMINI_KEY")
|
|
(gptel-make-gemini "Gemini"
|
|
:key (getenv "GEMINI_KEY")
|
|
:stream t))
|
|
|
|
(gptel-make-openai "llama.cpp"
|
|
:stream t
|
|
:protocol "http"
|
|
:host "127.0.0.1:1945"
|
|
:models '("Qwen2.5-Coder"
|
|
"Qwen3.5-0.8B"
|
|
"Qwen3.5-4B"
|
|
"Qwen3.5-9B"
|
|
"Qwen3.6-27B"
|
|
"Qwen3Coder-Q2"
|
|
"Qwen3Coder"
|
|
"QwenSeek-2B"
|
|
"gemma-4-12B"
|
|
"gemma-4-26B-Q2"
|
|
"gemma-4-26B"
|
|
"gemma-4-31B-Q2"
|
|
"gemma-4-31B"
|
|
"gemma-4-E2B"
|
|
"gemma-4-E4B-UNCENSORED"
|
|
"gemma-4-E4B"
|
|
"gptoss-20b"))
|
|
|
|
(gptel-make-ollama "Ollama"
|
|
:host "localhost:11434"
|
|
:stream t
|
|
:request-params '(:think :json-false)
|
|
:models '("gemma4:31b-cloud"
|
|
"glm-5.1:cloud"
|
|
"kimi-k2.6:cloud"
|
|
"gpt-oss:20b-cloud"
|
|
"minimax-m2.5:cloud"
|
|
"qwen3-coder-next:cloud"))
|
|
|
|
;; Backend opencode-go: suscripción OpenCode Go vía endpoint OpenAI-compatible
|
|
(defun kj-opencode-go-key ()
|
|
"Return the OpenCode Go API key from opencode's auth file."
|
|
(let ((auth (with-temp-buffer
|
|
(insert-file-contents "~/.local/share/opencode/auth.json")
|
|
(json-parse-buffer :object-type 'alist))))
|
|
(alist-get 'key (alist-get 'opencode-go auth))))
|
|
|
|
(gptel-make-openai "OpenCode Go"
|
|
:host "opencode.ai"
|
|
:endpoint "/zen/go/v1/chat/completions"
|
|
:key #'kj-opencode-go-key
|
|
:stream t
|
|
:models '(minimax-m3 minimax-m2.7 minimax-m2.5
|
|
kimi-k3 kimi-k2.7-code kimi-k2.6 kimi-k2.5
|
|
glm-5.2 glm-5.1 glm-5
|
|
deepseek-v4-pro deepseek-v4-flash
|
|
qwen3.7-max qwen3.8-max qwen3.7-plus qwen3.6-plus qwen3.5-plus
|
|
mimo-v2-pro mimo-v2-omni mimo-v2.5-pro mimo-v2.5
|
|
hy3 hy3-preview
|
|
gpt-5.6-luna grok-4.5))
|
|
|
|
;; Modelos gratuitos de OpenCode Go, servidos vía el endpoint /zen/v1
|
|
(gptel-make-openai "OpenCode Free"
|
|
:host "opencode.ai"
|
|
:endpoint "/zen/v1/chat/completions"
|
|
:key #'kj-opencode-go-key
|
|
:stream t
|
|
:models '(big-pickle longcat-2.0-free deepseek-v4-flash-free mimo-v2.5-free
|
|
north-mini-code-free nemotron-3-ultra-free
|
|
ling-3.0-flash-free laguna-s-2.1-free))
|
|
|
|
;; Backend y modelo por defecto
|
|
(setq gptel-backend (gptel-get-backend "OpenCode Go")
|
|
gptel-model 'deepseek-v4-flash)
|
|
|
|
(setq gptel-default-mode 'markdown-mode
|
|
gptel-prompt-prefix-alist
|
|
'((markdown-mode . "# User\n\n")
|
|
(org-mode . "* User\n\n")
|
|
(text-mode . "# User\n\n"))
|
|
gptel-response-prefix-alist
|
|
'((markdown-mode . "# AI\n\n")
|
|
(org-mode . "* AI\n\n")
|
|
(text-mode . "# AI\n\n"))
|
|
gptel-directives
|
|
'((default . "You are a large language model living in Emacs and a helpful assistant. Respond concisely in Spanish."))
|
|
)
|
|
|
|
(add-hook 'gptel-post-response-functions 'gptel-end-of-response)
|
|
;; El "thinking" de los modelos se muestra en los buffers gptel.
|
|
;; `ignore' (valor por defecto de gptel) lo muestra pero NO lo reenvía
|
|
;; al modelo en turnos posteriores (recomendado: evita llenar el
|
|
;; contexto, ver NEWS de gptel). Usar `t' si además se quiere
|
|
;; reenviarlo y `nil' para deshabilitarlo (útil para respuestas mas rápidas).
|
|
(setopt gptel-include-reasoning 'ignore)
|
|
|
|
(defun gptel-switch+model ()
|
|
"Switch to gptel backend and model in a single completion prompt."
|
|
(interactive)
|
|
(let (choices)
|
|
(dolist (pair gptel--known-backends)
|
|
(let* ((backend-name (car pair))
|
|
(backend (cdr pair))
|
|
(models
|
|
(and (fboundp 'gptel-backend-models)
|
|
(gptel-backend-models backend))))
|
|
(when models
|
|
(dolist (model models)
|
|
(push (cons ; (format "%s:%s" backend-name model)
|
|
(format "%s → %s"
|
|
(propertize backend-name 'face 'font-lock-keyword-face)
|
|
(propertize (symbol-name model) 'face 'font-lock-function-name-face))
|
|
(cons backend-name model))
|
|
choices)))))
|
|
(let* ((choice
|
|
(completing-read "Model: " (mapcar #'car choices) nil t))
|
|
(sel (cdr (assoc choice choices))))
|
|
(setq gptel-backend (cdr (assoc (car sel) gptel--known-backends))
|
|
gptel-model (cdr sel))
|
|
(message "gptel set to %s:%s" (car sel) (cdr sel)))))
|
|
|
|
;; Al cargar gptel, cargar gptel-agent para que sus presets estén
|
|
;; disponibles (p. ej. al reabrir una sesión guardada de agente).
|
|
(require 'gptel-agent)
|
|
)
|
|
|
|
;; (use-package copilot
|
|
;; :hook (prog-mode . copilot-mode)
|
|
;; :bind (:map copilot-completion-map
|
|
;; ("C-g" . 'copilot-clear-overlay)
|
|
;; ("C-<return>" . 'copilot-accept-completion)
|
|
;; ("S-<return>" . 'copilot-accept-completion-by-word)))
|
|
|
|
(use-package gptel-magit
|
|
:ensure t
|
|
:hook (magit-mode . gptel-magit-install)
|
|
:config
|
|
;; gptel-magit no sabe manejar el contenido de "reasoning": gptel entrega
|
|
;; el thinking al callback como (reasoning . TEXTO) y gptel-magit lo
|
|
;; inserta como si fuera la respuesta final, fallando con
|
|
;; Wrong type argument: char-or-string-p, (reasoning . "...")
|
|
;; (https://github.com/ragnard/gptel-magit/issues/8). Esto ocurre
|
|
;; aunque `gptel-include-reasoning' sea nil, porque gptel entrega las
|
|
;; celdas (reasoning . ...) al callback de forma incondicional (solo
|
|
;; controla si se insertan en el buffer gptel y si se reenvían).
|
|
;; En vez de deshabilitar el thinking globalmente, sanitizamos la
|
|
;; respuesta en el callback: se descartan las celdas de reasoning y solo
|
|
;; se deja pasar la respuesta final (string). Así el thinking sigue
|
|
;; activo en las sesiones gptel normales.
|
|
(defun kj-gptel-magit--string-response-callback (callback)
|
|
"Return a callback that forwards only string responses to CALLBACK.
|
|
Discards the (reasoning . TEXT) cells through which gptel delivers the
|
|
thinking content to the callback (see `gptel-request')."
|
|
(lambda (response &rest args)
|
|
(when (stringp response)
|
|
(apply callback response args))))
|
|
|
|
(defun kj-gptel-magit--sanitize-callback (orig-fn &rest args)
|
|
"Around advice for `gptel-magit--request'.
|
|
Wraps the :callback argument of ARGS so that responses with reasoning
|
|
content do not break gptel-magit."
|
|
(let ((pos (cl-position :callback args)))
|
|
(when (and pos (functionp (nth (1+ pos) args)))
|
|
(setf (nth (1+ pos) args)
|
|
(kj-gptel-magit--string-response-callback
|
|
(nth (1+ pos) args)))))
|
|
(apply orig-fn args))
|
|
|
|
(advice-add 'gptel-magit--request :around
|
|
#'kj-gptel-magit--sanitize-callback))
|
|
|
|
(use-package gptel-autocomplete
|
|
:defer nil
|
|
:ensure (:host github :repo "JDNdeveloper/gptel-autocomplete")
|
|
:bind (("M-<return>" . gptel-complete)
|
|
:map gptel-autocomplete-completion-map
|
|
("C-<return>" . gptel-accept-completion)))
|
|
|
|
(use-package gptel-agent
|
|
:ensure t
|
|
:defer nil
|
|
:init
|
|
(defvar kj-gptel-agents-dir
|
|
(expand-file-name "configs/gptel-agents" user-emacs-directory)
|
|
"Directory with the agents and prompts ported from opencode for gptel-agent.")
|
|
|
|
(defcustom kj-gptel-opencode-small-model nil
|
|
"Model for session titles/summaries (must exist in the active backend).
|
|
nil uses the current session's model."
|
|
:type '(choice (const :tag "Current session model" nil) string)
|
|
:group 'gptel)
|
|
|
|
(defcustom kj-gptel-plans-dir
|
|
(expand-file-name "gptel-plans" user-emacs-directory)
|
|
"Directory where gptel-agent plans are stored, outside the project.
|
|
Plans are named <hash>--<project>--<name>.md, where <hash> is a
|
|
truncated md5 of the project root, so plans never pollute the project
|
|
directory and can be mapped back to their project."
|
|
:type 'directory
|
|
:group 'gptel)
|
|
(defun kj-gptel-load-agents-md ()
|
|
"Add AGENTS.md rules to the current gptel buffer context.
|
|
Loads AGENTS.md from the project root and from `user-emacs-directory',
|
|
if they exist."
|
|
(interactive)
|
|
(require 'gptel-context nil t)
|
|
(let ((root (or (when-let* ((proj (project-current))) (project-root proj))
|
|
(vc-root-dir)
|
|
default-directory))
|
|
(seen '()))
|
|
(dolist (f (list (expand-file-name "AGENTS.md" root)
|
|
(expand-file-name "AGENTS.md" user-emacs-directory)))
|
|
(when (and (file-readable-p f) (not (member f seen)))
|
|
(push f seen)
|
|
(condition-case nil
|
|
(gptel-context-add-file f)
|
|
(error (message "kj-gptel: could not add AGENTS.md %s" f)))))))
|
|
|
|
(defun kj-gptel--agent-buffer-setup ()
|
|
"Load AGENTS.md rules when a gptel-agent session buffer is created.
|
|
Fires for new sessions (buffer *gptel-agent:*) and for sessions
|
|
restored from a saved file (gptel--preset of the agent)."
|
|
(when (and (bound-and-true-p gptel-mode)
|
|
(or (string-prefix-p "*gptel-agent" (buffer-name))
|
|
(and (boundp 'gptel--preset)
|
|
(memq gptel--preset '(gptel-agent gptel-plan)))))
|
|
(kj-gptel-load-agents-md)))
|
|
(defun kj-gptel--header-button-action ()
|
|
"Return the action function of the `[Agent]' / `[Plan]' header button, or nil.
|
|
Invoking it is exactly what clicking the button does. The button lives in
|
|
gptel-agent's header, rendered as (:eval (funcall DISPLAY))."
|
|
(let* ((elt (and (listp header-line-format) (car header-line-format)))
|
|
(expr (and (listp elt) (eq (car elt) :eval) (cadr elt)))
|
|
(fn (and (listp expr) (eq (car expr) 'funcall) (cadr expr)))
|
|
(str (and (functionp fn) (funcall fn))))
|
|
(or (and (stringp str)
|
|
(catch 'found
|
|
(dotimes (i (length str))
|
|
(let ((action (get-text-property i 'action str)))
|
|
(when (functionp action)
|
|
(throw 'found action))))))
|
|
;; Fallback: render vía `format-mode-line'
|
|
(let ((str2 (ignore-errors (format-mode-line header-line-format))))
|
|
(and (stringp str2)
|
|
(catch 'found
|
|
(dotimes (i (length str2))
|
|
(let ((action (get-text-property i 'action str2)))
|
|
(when (functionp action)
|
|
(throw 'found action))))))))))
|
|
|
|
(defun kj-gptel-toggle-plan-agent (&optional _data)
|
|
"Toggle between the default agent (build) and plan mode.
|
|
Equivalent to clicking the header button: it invokes the same action
|
|
that gptel-agent uses for that button. DATA is ignored (compatibility
|
|
with `buttonize')."
|
|
(interactive)
|
|
(unless (bound-and-true-p gptel-mode)
|
|
(user-error "You are not in a gptel session"))
|
|
(let ((action (kj-gptel--header-button-action)))
|
|
(if action
|
|
(funcall action nil) ; exactamente lo que hace el clic
|
|
;; Sin header disponible: aplicamos directamente el toggle
|
|
(gptel--apply-preset
|
|
(if (eq gptel--preset 'gptel-plan) 'gptel-agent 'gptel-plan)
|
|
(lambda (sym val) (set (make-local-variable sym) val)))))
|
|
(message "Mode %s activated" (if (eq gptel--preset 'gptel-plan) "plan" "build")))
|
|
|
|
(defun kj-gptel--eglot-start ()
|
|
"Start eglot immediately for the current buffer if it is not managed.
|
|
Unlike `eglot-ensure', this connects right away instead of waiting for a
|
|
`post-command-hook', so gptel tools can use LSP on their first call."
|
|
(require 'eglot)
|
|
(unless eglot--managed-mode
|
|
(condition-case err
|
|
(apply #'eglot--connect (eglot--guess-contact))
|
|
(error (message "kj-gptel: error starting eglot: %s"
|
|
(error-message-string err))))))
|
|
|
|
(defun kj-gptel--lsp-buffer (&optional path)
|
|
"Return a buffer with a live eglot server for PATH.
|
|
When PATH is nil, return any buffer visiting a file with a live server,
|
|
preferring one in the current project."
|
|
(if path
|
|
(let ((buf (or (find-buffer-visiting path)
|
|
(and (file-exists-p path) (find-file-noselect path)))))
|
|
(when buf
|
|
(with-current-buffer buf
|
|
(unless (eglot-current-server)
|
|
(kj-gptel--eglot-start))
|
|
buf)))
|
|
(let* ((root (and-let* ((proj (project-current))) (project-root proj)))
|
|
(in-project
|
|
(cl-find-if
|
|
(lambda (b)
|
|
(with-current-buffer b
|
|
(and (eglot-current-server)
|
|
(buffer-file-name b)
|
|
root
|
|
(file-in-directory-p (buffer-file-name b) root))))
|
|
(buffer-list)))
|
|
(any
|
|
(cl-find-if
|
|
(lambda (b)
|
|
(with-current-buffer b
|
|
(and (eglot-current-server) (buffer-file-name b))))
|
|
(buffer-list))))
|
|
(or in-project any))))
|
|
|
|
(defun kj-gptel-lsp-diagnostics (path)
|
|
"Return LSP diagnostics (errors/warnings) for the file at PATH.
|
|
Uses eglot and flymake; starts the language server if needed."
|
|
(require 'flymake nil t)
|
|
(let ((buf (kj-gptel--lsp-buffer path)))
|
|
(if (not buf)
|
|
(format "Could not open file %s" path)
|
|
(with-current-buffer buf
|
|
(if (not (eglot-current-server))
|
|
(format "No LSP server for %s (mode %s)."
|
|
path major-mode)
|
|
(let ((deadline (+ (float-time) 5)))
|
|
(while (and (< (float-time) deadline)
|
|
(null (flymake-diagnostics (point-min) (point-max))))
|
|
(sit-for 0.2)))
|
|
(when (null (flymake-diagnostics (point-min) (point-max)))
|
|
;; Forzar la recolección de diagnósticos del servidor
|
|
(ignore-errors (flymake-start))
|
|
(let ((deadline (+ (float-time) 5)))
|
|
(while (and (< (float-time) deadline)
|
|
(null (flymake-diagnostics (point-min) (point-max))))
|
|
(sit-for 0.2))))
|
|
(let ((diags (flymake-diagnostics (point-min) (point-max))))
|
|
(if (null diags)
|
|
(format "No diagnostics for %s." path)
|
|
(mapconcat
|
|
(lambda (d)
|
|
(let ((beg (flymake-diagnostic-beg d))
|
|
(type (flymake-diagnostic-type d)))
|
|
(save-excursion
|
|
(goto-char beg)
|
|
(format "%s:%d:%d [%s] %s"
|
|
path
|
|
(line-number-at-pos)
|
|
(1+ (current-column))
|
|
(or type "unknown")
|
|
(flymake-diagnostic-text d)))))
|
|
diags "\n"))))))))
|
|
|
|
(defun kj-gptel-lsp-symbols (query &optional path)
|
|
"Search workspace symbols matching QUERY via LSP (eglot)."
|
|
(require 'eglot)
|
|
(let ((buf (kj-gptel--lsp-buffer path)))
|
|
(if (not buf)
|
|
"There is no buffer with a connected LSP server in the project."
|
|
(condition-case err
|
|
(with-current-buffer buf
|
|
(let ((res (eglot--workspace-symbols query)))
|
|
(if (null res)
|
|
(format "No symbols matching \"%s\"." query)
|
|
(mapconcat #'substring-no-properties res "\n"))))
|
|
(error (format "Error in the LSP query: %S" err))))))
|
|
|
|
(defun kj-gptel--project-root ()
|
|
"Return the current project root, or `default-directory'."
|
|
(or (when-let* ((proj (project-current))) (project-root proj))
|
|
(vc-root-dir)
|
|
default-directory))
|
|
|
|
(defun kj-gptel--read-prompt (name)
|
|
"Read the opencode prompt file NAME (e.g. \"title.txt\") from the agents dir."
|
|
(string-trim
|
|
(with-temp-buffer
|
|
(insert-file-contents
|
|
(expand-file-name (format "prompts/%s" name) kj-gptel-agents-dir))
|
|
(buffer-string))))
|
|
|
|
(defun kj-gptel--patch-apply (diff root level)
|
|
"Apply DIFF in ROOT with `patch -pLEVEL'. Return (EXIT . OUTPUT)."
|
|
(let* ((tmp (make-temp-file "gptel-patch-" nil ".diff"))
|
|
(default-directory root))
|
|
(with-temp-file tmp (insert diff))
|
|
(let ((out (shell-command-to-string
|
|
(format "patch --batch --forward --fuzz=3 -p%d < %s 2>&1; echo PATCH_EXIT=$?"
|
|
level tmp))))
|
|
(delete-file tmp)
|
|
(if (string-match "PATCH_EXIT=\\([0-9]+\\)" out)
|
|
(cons (string-to-number (match-string 1 out))
|
|
(replace-regexp-in-string "PATCH_EXIT=[0-9]+" "" out))
|
|
(cons -1 out)))))
|
|
|
|
(defun kj-gptel-apply-patch (diff)
|
|
"Apply the unified DIFF to the project and return the result.
|
|
Tries `patch -p1' first and falls back to `-p0'."
|
|
(let* ((root (kj-gptel--project-root))
|
|
(r1 (kj-gptel--patch-apply diff root 1)))
|
|
(if (zerop (car r1))
|
|
(format "Patch applied successfully.\n%s" (cdr r1))
|
|
(let ((r0 (kj-gptel--patch-apply diff root 0)))
|
|
(if (zerop (car r0))
|
|
(format "Patch applied successfully (with -p0).\n%s" (cdr r0))
|
|
(format "Could not apply the patch (exit %s).\n%s"
|
|
(car r1) (cdr r1)))))))
|
|
|
|
(defun kj-gptel-plan-write (filename content)
|
|
"Write CONTENT to a plan file for the current project and return status.
|
|
Plans are stored in `kj-gptel-plans-dir' (outside the project), named
|
|
<md5-prefix>--<project>--<name>.md so that each project keeps its own
|
|
plans without adding extra files to the project directory."
|
|
(let* ((root (kj-gptel--project-root))
|
|
(hash (substring (md5 root) 0 12))
|
|
(project (replace-regexp-in-string
|
|
"[^A-Za-z0-9._-]" "-"
|
|
(file-name-nondirectory (directory-file-name root))))
|
|
(name (file-name-nondirectory filename))
|
|
(file (expand-file-name
|
|
(format "%s--%s--%s" hash project name)
|
|
kj-gptel-plans-dir)))
|
|
(make-directory kj-gptel-plans-dir t)
|
|
(with-temp-file file (insert content))
|
|
(format "Plan saved to %s" file)))
|
|
|
|
(defun kj-gptel-opencode-summary ()
|
|
"Generate a PR-style summary of the current gptel session (prompt summary.txt)."
|
|
(interactive)
|
|
(unless (bound-and-true-p gptel-mode)
|
|
(user-error "You are not in a gptel session"))
|
|
(let* ((buf (current-buffer))
|
|
(transcript (buffer-substring-no-properties (point-min) (point-max)))
|
|
(out (get-buffer-create "*opencode-summary*")))
|
|
(with-current-buffer out
|
|
(let ((inhibit-read-only t)) (erase-buffer))
|
|
(gptel-mode 1)
|
|
(setq-local gptel-backend (with-current-buffer buf gptel-backend)
|
|
gptel-model (or kj-gptel-opencode-small-model
|
|
(with-current-buffer buf gptel-model))))
|
|
(display-buffer out)
|
|
(gptel-request transcript
|
|
:buffer out
|
|
:stream t
|
|
:system (kj-gptel--read-prompt "summary.txt")
|
|
:callback (lambda (_info &rest _)
|
|
(message "Summary generated in *opencode-summary*")))))
|
|
|
|
(defun kj-gptel-opencode-title ()
|
|
"Generate a title for the current gptel session (prompt title.txt)."
|
|
(interactive)
|
|
(unless (bound-and-true-p gptel-mode)
|
|
(user-error "You are not in a gptel session"))
|
|
(let* ((buf (current-buffer))
|
|
(transcript (buffer-substring-no-properties (point-min) (point-max)))
|
|
(tmp (generate-new-buffer " *gptel-title*")))
|
|
(with-current-buffer tmp
|
|
(gptel-mode 1)
|
|
(setq-local gptel-backend (with-current-buffer buf gptel-backend)
|
|
gptel-model (or kj-gptel-opencode-small-model
|
|
(with-current-buffer buf gptel-model))))
|
|
(gptel-request transcript
|
|
:buffer tmp
|
|
:stream nil
|
|
:system (kj-gptel--read-prompt "title.txt")
|
|
:callback (lambda (_info &rest _)
|
|
(let ((title (string-trim
|
|
(with-current-buffer tmp (buffer-string)))))
|
|
(kill-buffer tmp)
|
|
(with-current-buffer buf
|
|
(rename-buffer (format "*gptel-agent:%s*" title) t))
|
|
(message "Title: %s" title))))))
|
|
:custom
|
|
(kj-gptel-opencode-small-model nil)
|
|
:bind
|
|
(("C-c g" . gptel-agent)
|
|
:map gptel-mode-map
|
|
("C-c C-a" . kj-gptel-toggle-plan-agent))
|
|
:hook (gptel-mode . kj-gptel--agent-buffer-setup)
|
|
:config
|
|
(add-to-list 'gptel-agent-dirs kj-gptel-agents-dir t)
|
|
(setq gptel-agent-compact-prompt
|
|
(string-trim
|
|
(with-temp-buffer
|
|
(insert-file-contents
|
|
(expand-file-name "prompts/compaction.txt" kj-gptel-agents-dir))
|
|
(buffer-string))))
|
|
;; En modo agente, que no pidan confirmación las tools de uso frecuente
|
|
;; (comandos y escritura de archivos); Eval y Agent siguen confirmando.
|
|
(dolist (name '("Bash" "Edit" "Write" "Insert" "Mkdir"))
|
|
(let ((tool (gptel-get-tool name)))
|
|
(when tool
|
|
(setf (gptel-tool-confirm tool) nil))))
|
|
|
|
;; Sobrescribir la tool "Eval" para que sea segura: con `inhibit-interaction',
|
|
;; las llamadas interactivas (read-*, y-or-n-p, call-interactively, ...)
|
|
;; lanzan error en vez de quedarse esperando input y colgar al agente.
|
|
(gptel-make-tool
|
|
:name "Eval"
|
|
:function
|
|
(lambda (expression)
|
|
(let ((standard-output (generate-new-buffer " *gptel-agent-eval-elisp*"))
|
|
(result nil) (output nil))
|
|
(unwind-protect
|
|
(condition-case err
|
|
(progn
|
|
(let ((inhibit-interaction t))
|
|
(setq result (eval (read expression) t)))
|
|
(when (> (buffer-size standard-output) 0)
|
|
(setq output (with-current-buffer standard-output (buffer-string))))
|
|
(concat
|
|
(format "Result:\n%S" result)
|
|
(and output (format "\n\nSTDOUT:\n%s" output))))
|
|
((error user-error)
|
|
(concat
|
|
(if (eq (car err) 'inhibited-interaction)
|
|
"Error: the expression tried to prompt the user for input interactively, which is blocked in the Eval tool."
|
|
(format "Error: eval failed with error %S: %S" (car err) (cdr err)))
|
|
(and output (format "\n\nSTDOUT:\n%s" output)))))
|
|
(kill-buffer standard-output))))
|
|
:description "Evaluate Elisp EXPRESSION and return result and any printed output.
|
|
|
|
EXPRESSION can be anything to evaluate. It can be a function call, a
|
|
variable, a quasi-quoted expression. The only requirement is that only
|
|
the first sexp will be read and evaluated, so if you need to evaluate
|
|
multiple expressions, make one call per expression. Do not combine
|
|
expressions using progn etc. Just go expression by expression and try
|
|
to make standalone single expressions.
|
|
|
|
Interactive operations are blocked: if the expression tries to prompt
|
|
for input (read-*, y-or-n-p, call-interactively, etc.) it will error
|
|
instead of waiting.
|
|
|
|
The return value is formatted to a string using %S, so a string will be
|
|
returned as an escaped embedded string and literal forms will be
|
|
compatible with `read' where possible. Some forms have no printed
|
|
representation that can be read and will be represented with
|
|
#<hash-notation> instead.
|
|
|
|
Output from `print', `prin1', and `princ' is captured and returned as STDOUT.
|
|
Use `print' for diagnostic output, not `message' (which goes to *Messages*
|
|
and is not captured)."
|
|
:args '((:name "expression"
|
|
:type string
|
|
:description "A single elisp sexp to evaluate."))
|
|
:category "gptel-agent"
|
|
:confirm t
|
|
:include t)
|
|
|
|
(gptel-agent-update)
|
|
|
|
;; Herramientas del agente (equivalente a las tools de opencode)
|
|
(gptel-make-tool
|
|
:name "lsp_diagnostics"
|
|
:function #'kj-gptel-lsp-diagnostics
|
|
:description "Get the LSP diagnostics (errors and warnings) of a file via the language server (eglot).
|
|
|
|
Use it to know if a file has errors after editing it or to check the state of the code. It takes the file path and returns one line per diagnostic in the format path:line:column [type] message. If the LSP server is not connected for that file, it tries to start it."
|
|
:args '((:name "path"
|
|
:type string
|
|
:description "Path of the file to get diagnostics from."))
|
|
:category "gptel-agent"
|
|
:include t)
|
|
|
|
(gptel-make-tool
|
|
:name "lsp_symbols"
|
|
:function #'kj-gptel-lsp-symbols
|
|
:description "Search workspace symbols (functions, classes, variables, etc.) via LSP (eglot).
|
|
|
|
Useful for locating definitions by name or seeing which symbols exist. It takes a query (pattern) and returns the list of matching symbols in the format \"container name Type\"."
|
|
:args '((:name "query"
|
|
:type string
|
|
:description "Pattern to search in the workspace symbols.")
|
|
(:name "path"
|
|
:type string
|
|
:description "Path of a project file whose LSP server to use (optional)."
|
|
:optional t))
|
|
:category "gptel-agent"
|
|
:include t)
|
|
|
|
(gptel-make-tool
|
|
:name "apply_patch"
|
|
:function #'kj-gptel-apply-patch
|
|
:description "Apply a unified diff to the project files.
|
|
|
|
Takes a unified diff (git format or --- a/ / +++ b/). It is applied with the patch tool (tries -p1 and then -p0). Use it for large edits or edits that touch multiple files, instead of multiple Edit calls. Modifies project files."
|
|
:args '((:name "diff"
|
|
:type string
|
|
:description "The unified diff to apply, with file headers (--- a/... and +++ b/...)."))
|
|
:category "gptel-agent"
|
|
:confirm t
|
|
:include t)
|
|
|
|
(gptel-make-tool
|
|
:name "plan_write"
|
|
:function #'kj-gptel-plan-write
|
|
:description "Save the current plan as a Markdown file in the Emacs directory, outside the project.
|
|
|
|
Use it when you have drafted a plan, to persist it to disk and be able to resume it later. Plans are stored in `kj-gptel-plans-dir' (inside the Emacs directory) named <hash>--<project>--<name>.md, where <hash> is a truncated md5 of the project root, so each project keeps its own plans without adding files to the project directory. Does not modify any project file."
|
|
:args '((:name "filename"
|
|
:type string
|
|
:description "Name of the plan file (e.g. refactor-auth.md).")
|
|
(:name "content"
|
|
:type string
|
|
:description "Markdown content of the plan."))
|
|
:category "gptel-agent"
|
|
:confirm t
|
|
:include t))
|
|
|
|
(use-package macher
|
|
:ensure (:host github :repo "kmontag/macher")
|
|
:custom
|
|
(macher-action-buffer-ui 'org))
|
|
|
|
(provide 'init-ai)
|
|
;;; init-ai.el ends here
|