;;; init-gptel.el --- ai -*- lexical-binding: t -*- ;;; Commentary: ;;; Code: (use-package gptel :hook (gptel-mode-hook . visual-line-mode) :config (setq gptel-log-level t) (setq gptel-default-mode 'org-mode) (add-hook 'gptel-post-response-functions 'gptel-end-of-response) (add-hook 'gptel-post-stream-hook 'gptel-auto-scroll) (setq gptel-prompt-prefix-alist '((markdown-mode . "## User: ") (org-mode . "** User: ") (text-mode . "## User: "))) (gptel-make-tool :name "read_buffer" ; javascript-style snake_case name :function (lambda (buffer) ; the function that will run (unless (buffer-live-p (get-buffer buffer)) (error "error: buffer %s is not live." buffer)) (with-current-buffer buffer (buffer-substring-no-properties (point-min) (point-max)))) :description "return the contents of an emacs buffer" :args (list '(:name "buffer" :type string ; :type value must be a symbol :description "the name of the buffer whose contents are to be retrieved")) :category "emacs") ; An arbitrary label for grouping (gptel-make-openai "gptel-Kimi-Code" :host "api.kimi.com" :endpoint "/coding/v1/chat/completions" :protocol "https" :key (lambda () (auth-source-pick-first-password :host "api.kimi.com" :user "apikey")) :stream t :models '(kimi-for-coding) :header (lambda (_) (when-let ((key (gptel--get-api-key))) `(("User-Agent" . "RooCode/3.30.3") ("HTTP-Referer" . "https://github.com/RooVetGit/Roo-Cline") ("X-Title" . "Roo Code") ("Authorization" . ,(concat "Bearer " key)))))) (setq gptel-model 'kimi-for-coding gptel-backend (gptel-get-backend "gptel-Kimi-Code")) (defun my-gptel--sanitize-filename (name) "Clean NAME into a safe kebab-case filename string." (let ((clean (downcase name))) (setq clean (replace-regexp-in-string "[^a-z0-9_-]" "-" clean)) (setq clean (replace-regexp-in-string "^-+\\|[-]+$" "" clean)) (setq clean (replace-regexp-in-string "-[-]+" "-" clean)) (string-trim clean))) (defun my-gptel-save-session () "Save current gptel session to ~/gptel/sessions/ with AI-generated filename. The filename is prefixed with a YYYYMMDD-HHmmss timestamp and generated from the last 1500 characters of the buffer." (interactive) (unless gptel-mode (user-error "Not a gptel session buffer")) (let* ((buf (current-buffer)) (content (with-current-buffer buf (buffer-substring-no-properties (point-min) (point-max)))) (tail (substring content (max 0 (- (length content) 1500)))) (prompt-text (format "Based on the following conversation, generate a short kebab-case filename (3 to 5 lowercase English words, hyphen-separated, no file extension). Output ONLY the filename, nothing else.\n\n%s" tail))) (message "Generating filename with AI...") (let ((fsm (gptel-request prompt-text :system "You are a filename generator. Output ONLY a short kebab-case filename (3-5 lowercase English words separated by hyphens). No explanations, no quotes, no punctuation, no file extension." :stream nil :callback (lambda (response info) (condition-case err (cond ((null response) (message "Failed to generate filename: %s" (or (plist-get info :status) "unknown error"))) ((not (stringp response)) (message "Unexpected response type: %S" response)) (t (let* ((generated (my-gptel--sanitize-filename (string-trim response))) (generated (if (string-empty-p generated) "gptel-session" generated)) (timestamp (format-time-string "%Y%m%d-%H%M%S")) (dir (expand-file-name "~/gptel/sessions/")) (base (concat timestamp "-" generated)) (file (expand-file-name (concat base ".org") dir))) ;; handle name collision (when (file-exists-p file) (setq base (concat timestamp "-" generated "-" (format-time-string "%S"))) (setq file (expand-file-name (concat base ".org") dir))) (unless (file-directory-p dir) (make-directory dir t)) ;; write-file triggers gptel--save-state to preserve metadata (with-current-buffer buf (write-file file)) (if (file-exists-p file) (progn (pop-to-buffer (find-file-noselect file)) (message "Saved gptel session to %s" file)) (message "ERROR: File was not created: %s" file))))) (error (message "gptel save-session error: %S" err))))))) (unless (plist-get (gptel-fsm-info fsm) :callback) (message "WARNING: gptel-request did not register callback!"))))) (with-eval-after-load 'gptel (define-key gptel-mode-map (kbd "C-c C-s") #'my-gptel-save-session))) (use-package gptel-agent :init (with-eval-after-load 'gptel (require 'gptel-agent)) :config (add-to-list 'gptel-agent-dirs "~/.emacs.d/lisp/" t) (gptel-agent-update)) (use-package gptel-magit :straight (gptel-magit :type git :host github :repo "ArthurHeymans/gptel-magit") :hook (magit-mode . gptel-magit-install) :init (with-eval-after-load 'gptel (gptel-make-openai "gptel-Kimi-Code-magit" :host "api.kimi.com" :endpoint "/coding/v1/chat/completions" :protocol "https" :key (lambda () (auth-source-pick-first-password :host "api.kimi.com" :user "apikey")) :stream t :models '((kimi-for-coding)) :request-params '(:thinking (:type "disabled")) :header (lambda (_) (when-let ((key (gptel--get-api-key))) `(("User-Agent" . "RooCode/3.30.3") ("HTTP-Referer" . "https://github.com/RooVetGit/Roo-Cline") ("X-Title" . "Roo Code") ("Authorization" . ,(concat "Bearer " key)))))) (setq gptel-magit-model 'kimi-for-coding gptel-magit-backend (gptel-get-backend "gptel-Kimi-Code-magit")))) (provide 'init-gptel) ;;; init-gptel.el ends here