commit - 01d0687853d8d8f5b6330e1f4868ab44f4949c9e
commit + 53b6364d5d04cee453e08bdb4300c821032b5a18
blob - 64f55fd228a5a0582d8c89894c334ad7cce4f0e9
blob + 3ead197b17b7252ef3b872d0955548377ea4a1e2
--- config.org
+++ config.org
(error "Emacs configuration only works with Emacs 29 and newer; you have version %s"
emacs-major-version))
- (setopt initial-major-mode 'fundamental-mode)
- (setopt inhibit-startup-screen t)
- (setopt initial-scratch-message nil)
- (setopt display-time-default-load-average nil)
+ (setopt initial-major-mode 'fundamental-mode
+ inhibit-startup-screen t
+ initial-scratch-message nil
+ display-time-default-load-average nil
+ sentence-end-double-space nil
+ use-short-answers t)
- (setopt auto-revert-avoid-polling t)
- (setopt auto-revert-interval 5)
- (setopt auto-revert-check-vc-info t)
+ (setopt auto-revert-avoid-polling t
+ auto-revert-interval 5
+ auto-revert-check-vc-info t)
(global-auto-revert-mode 1)
(savehist-mode 1)
(windmove-default-keybindings 'control)
- (setopt sentence-end-double-space nil)
-
(when (display-graphic-p)
(context-menu-mode -1)
(tool-bar-mode -1)
(menu-bar-mode -1)
- (scroll-bar-mode -1)
- (set-frame-parameter nil 'font "Go Mono-14"))
+ (scroll-bar-mode -1))
+ ;; Use Go Mono everywhere. `default' is set directly here; `fixed-pitch'
+ ;; and `variable-pitch' must be pointed at it explicitly so that e.g.
+ ;; `variable-pitch-mode' in org-mode still renders in Go Mono.
+ (set-face-attribute 'default nil :family "Go Mono" :height 140)
+ (set-face-attribute 'fixed-pitch nil :family "Go Mono" :height 1.0)
+ (set-face-attribute 'variable-pitch nil :family "Go Mono" :height 1.0)
+
(unless (display-graphic-p)
(xterm-mouse-mode 1))
(cua-mode 1)
- (setopt display-line-numbers-width 3)
+ (keymap-set global-map "C-x k" #'kill-current-buffer)
+ (add-hook 'before-save-hook #'whitespace-cleanup)
+
+ (setopt display-line-numbers-width 3
+ tab-bar-show 1)
(add-hook 'prog-mode-hook #'display-line-numbers-mode)
(add-hook 'text-mode-hook #'visual-line-mode)
(dolist (hook '(text-mode-hook prog-mode-hook))
(add-hook hook #'hl-line-mode))
- (setopt tab-bar-show 1)
-
- ;; Start the Emacs server if it is not already running so `emacsclient' can
- ;; attach to this session. Remove the server socket when this Emacs exits.
(require 'server)
(unless (server-running-p)
(server-start))
(lock-dir (expand-file-name "locks/" "~/.cache/emacs")))
(make-directory auto-save-dir t)
(make-directory lock-dir t)
- (setopt auto-save-file-name-transforms `((".*" ,auto-save-dir t)))
- (setopt lock-file-name-transforms `((".*" ,lock-dir t))))
+ (setopt auto-save-file-name-transforms `((".*" ,auto-save-dir t))
+ lock-file-name-transforms `((".*" ,lock-dir t))))
#+end_src
* UI
:defer 1
:config (which-key-mode))
+ (use-package smart-mode-line
+ :straight t
+ :demand t
+ :custom
+ (sml/no-confirm-load-theme t)
+ (sml/theme 'respectful)
+ :config
+ (sml/setup))
+
+ (use-package minions
+ :straight t
+ :demand t
+ :config
+ (minions-mode))
+
+ ;; Lets `:diminish' hide minor-mode lighters from the mode line.
+ (use-package diminish
+ :straight t
+ :demand t)
+
(add-to-list 'tab-bar-format 'tab-bar-format-align-right 'append)
(add-to-list 'tab-bar-format 'tab-bar-format-global 'append)
- (setopt display-time-format "%a %F %T")
- (setopt display-time-interval 1)
+ (setopt display-time-format "%a %F %T"
+ display-time-interval 1)
(display-time-mode 1)
#+end_src
* Scrolling
#+begin_src emacs-lisp
- (setopt mouse-wheel-tilt-scroll t)
- (setopt mouse-wheel-flip-direction t)
- (setopt mouse-wheel-scroll-amount '(1 ((shift) . 5) ((control) . 0)))
- (setopt mouse-wheel-progressive-speed nil)
+ (setopt mouse-wheel-tilt-scroll t
+ mouse-wheel-flip-direction t
+ mouse-wheel-scroll-amount '(1 ((shift) . 5) ((control) . 0))
+ mouse-wheel-progressive-speed nil)
+ (setopt scroll-margin 4
+ scroll-conservatively 101
+ scroll-up-aggressively 0.01
+ scroll-down-aggressively 0.01
+ auto-window-vscroll nil)
+
(blink-cursor-mode -1)
(pixel-scroll-precision-mode 1)
- (setopt scroll-margin 4)
- (setopt scroll-conservatively 101)
- (setopt scroll-up-aggressively 0.01)
- (setopt scroll-down-aggressively 0.01)
- (setopt auto-window-vscroll nil)
#+end_src
* Appearance
:straight (circadian :type git :host github :repo "guidoschmidt/circadian.el")
:demand t
:config
- (setopt calendar-latitude 48.1386)
- (setopt calendar-longitude 17.1053)
- (setopt circadian-themes '((:sunrise . ef-orange)
+ (setopt calendar-latitude 48.1386
+ calendar-longitude 17.1053
+ circadian-themes '((:sunrise . ef-orange)
(:sunset . modus-vivendi)))
(circadian-setup))
#+end_src
* Completion
#+begin_src emacs-lisp
- (setopt completion-cycle-threshold 3)
- (setopt tab-always-indent 'complete)
+ (setopt completion-cycle-threshold 3
+ tab-always-indent 'complete)
(keymap-set minibuffer-mode-map "TAB" #'minibuffer-complete)
:straight t
:after eshell
:custom
- (eat-term-name "xterm")
+ ;; A universally-installed TERM, so shells eat spawns never look up the
+ ;; bundled `eat-truecolor' entry (which `tput' fails to find otherwise).
+ (eat-term-name "xterm-256color")
:config
(eat-eshell-mode)
(eat-eshell-visual-command-mode))
#+end_src
-* Development core
+#+begin_src emacs-lisp
+ (use-package tramp
+ :straight (:type built-in)
+ :defer t
+ :custom
+ (tramp-default-method "ssh")
+ (tramp-verbose 2)
+ (remote-file-name-inhibit-cache 60)
+ :config
+ (add-to-list 'tramp-remote-path 'tramp-own-remote-path))
+#+end_src
+* Core
+
#+begin_src emacs-lisp
(use-package treesit-auto
:straight t
:bind (("C-x g" . magit-status)))
#+end_src
-* Language modes
+* Languages
#+begin_src emacs-lisp
(use-package yaml-mode
:config (fset #'jsonrpc--log-event #'ignore))
#+end_src
+#+begin_src emacs-lisp
+ ;; GC and the file-name handler are restored on `emacs-startup-hook'
+ ;; (see early-init). Larger process reads make eglot/LSP feel snappier.
+ (setopt read-process-output-max (* 4 1024 1024)
+ process-adaptive-read-buffering nil)
+#+end_src
+
* Templates
#+begin_src emacs-lisp
:after denote)
#+end_src
-* Org
+* org-mode
#+begin_src emacs-lisp
(setopt org-directory (expand-file-name "~/.nb/notes")
(keymap-set denote-prefix-map "t" #'org-agenda-list)
(keymap-set denote-prefix-map "c" #'org-capture)
+ (defvar-keymap gptel-prefix-map
+ :doc "Prefix map for gptel commands.")
+ (keymap-set global-map "C-c L" gptel-prefix-map)
+ (keymap-set gptel-prefix-map "s" #'gptel-send)
+ (keymap-set gptel-prefix-map "m" #'gptel-menu)
+ (keymap-set gptel-prefix-map "r" #'gptel-rewrite)
+ (keymap-set gptel-prefix-map "a" #'gptel-add)
+ (keymap-set gptel-prefix-map "A" #'gptel-add-file)
+ (keymap-set gptel-prefix-map "p" #'gptel-preset)
+ (keymap-set gptel-prefix-map "t" #'gptel-tools)
+ (keymap-set gptel-prefix-map "n" #'mine/gptel-denote-session)
+
+ (defvar-keymap devops-prefix-map
+ :doc "Prefix map for devops / container / shell commands.")
+ (keymap-set global-map "C-c d" devops-prefix-map)
+ (keymap-set devops-prefix-map "d" #'docker)
+ (keymap-set devops-prefix-map "c" #'docker-compose)
+ (keymap-set devops-prefix-map "s" #'shell-command)
+ (keymap-set devops-prefix-map "b" #'org-babel-execute-buffer)
+ (keymap-set devops-prefix-map "B" #'org-babel-execute-subtree)
+ (keymap-set devops-prefix-map "t" #'ob-tmux-open)
+
(with-eval-after-load 'corfu
(keymap-set corfu-map "C-b" #'corfu-scroll-down)
(keymap-set corfu-map "C-f" #'corfu-scroll-up)
("C-c t s" . tmr-stop)))
#+end_src
-* Finalize
+* gptel
+LLMs in Emacs. A single backend talks to Ollama Cloud over its
+OpenAI-compatible endpoint; the API key is read from `auth-source'
+(default `~/.authinfo.gpg'), so no secret lives in this repository. On
+top of that we register web, filesystem and Emacs tools, and a set of
+presets (`@research', `@agent', per-model `@qwen' …) selectable per
+request.
+
+** Denote sessions and Ollama helpers
+
#+begin_src emacs-lisp
- ;; GC and the file-name handler are restored on `emacs-startup-hook'
- ;; (see early-init). Larger process reads make eglot/LSP feel snappier.
- (setopt read-process-output-max (* 4 1024 1024)
- process-adaptive-read-buffering nil)
+ (defun mine/gptel-denote-session ()
+ "Create a Denote note and start a gptel session in it."
+ (interactive)
+ (let ((title (read-string "GPTel session title: ")))
+ (denote title '("ai" "gptel"))
+ (gptel-mode)))
+
+ (defun mine/gptel-auto-save (_beg _end)
+ "Save current gptel buffer after each response, if it is a Denote note.
+Called from `gptel-post-response-functions', which passes the
+response beginning and end positions; we ignore both."
+ (when (and buffer-file-name
+ (derived-mode-p 'org-mode)
+ (string-prefix-p (expand-file-name denote-directory)
+ (file-name-directory buffer-file-name)))
+ (save-buffer)))
+
+ (add-hook 'gptel-post-response-functions #'mine/gptel-auto-save)
+
+ ;; --- Ollama Cloud REST helpers (shared by the gptel web tools) -------
+
+ (defun mine/ollama-api-key ()
+ "Return the Ollama Cloud API key stored in auth-source.
+Expects a line in `auth-sources' for host ollama.com, user apikey."
+ (or (auth-source-pick-first-password :host "ollama.com" :user "apikey")
+ (user-error "No Ollama API key in auth-source for host ollama.com")))
+
+ (defun mine/ollama-api-post (path payload)
+ "POST PAYLOAD (an alist) as JSON to Ollama API PATH, return parsed JSON.
+Signals an error on a non-2xx response. Runs synchronously."
+ (let* ((url-request-method "POST")
+ (url-request-extra-headers
+ `(("Content-Type" . "application/json")
+ ("Authorization" . ,(concat "Bearer " (mine/ollama-api-key)))))
+ (url-request-data (encode-coding-string (json-encode payload) 'utf-8))
+ (buf (url-retrieve-synchronously (concat "https://ollama.com" path)
+ 'silent nil 60)))
+ (unless buf (error "Ollama API request to %s timed out" path))
+ (unwind-protect
+ (with-current-buffer buf
+ (goto-char (point-min))
+ (let ((status (and (re-search-forward "^HTTP/[0-9.]+ \\([0-9]+\\)" nil t)
+ (string-to-number (match-string 1)))))
+ (unless (re-search-forward "\n\n" nil t)
+ (error "Malformed response from Ollama API"))
+ (let* ((json-object-type 'alist)
+ (json-array-type 'list)
+ (json-key-type 'symbol)
+ (body (json-read)))
+ (when (and status (>= status 400))
+ (error "Ollama API %s failed (HTTP %d): %s" path status
+ (or (alist-get 'error body) body)))
+ body)))
+ (kill-buffer buf))))
#+end_src
+
+** Client, backend, tools and presets
+
+#+begin_src emacs-lisp
+ (use-package gptel
+ :straight (gptel :type git :host github :repo "karthink/gptel")
+ :demand t
+ :bind (:map gptel-mode-map
+ ("C-c RET" . gptel-send)
+ ("C-c C-r" . gptel-rewrite)
+ ("C-c C-a" . gptel-add))
+ :custom
+ (gptel-use-curl t)
+ (gptel-stream t)
+ (gptel-default-mode 'org-mode)
+ (gptel-use-header-line t)
+ (gptel-org-ignore-elements '(property-drawer))
+ (gptel-directives
+ '((default . "You are a large language model living in Emacs and a helpful assistant. Respond concisely.")
+ (programming . "You are a careful programmer working from inside Emacs. Provide code, with minimal surrounding prose.")
+ (reviewing . "You are reviewing code or text in Emacs. Be terse, point out concrete issues, and suggest fixes.")
+ (literate . "You are helping with a literate configuration in an Org file. Prefer Org markup and explain only what is non-obvious.")
+ (chat . "You are a conversation partner. Respond concisely.")))
+ (gptel-use-tools t)
+ (gptel-confirm-tool-calls 'auto)
+ (gptel-include-tool-results 'auto)
+ :config
+ ;; `gptel-ollama' is loaded only so `ob-llm' keeps working; the backend
+ ;; itself is the OpenAI-compatible Ollama Cloud endpoint below.
+ (require 'gptel-ollama)
+ (require 'gptel-openai)
+ (gptel-make-openai "Ollama Cloud"
+ :host "ollama.com"
+ :protocol "https"
+ :endpoint "/v1/chat/completions"
+ :stream t
+ :key (lambda ()
+ (auth-source-pick-first-password
+ :host "ollama.com" :user "apikey"))
+ :models '(kimi-k2.7-code:cloud
+ kimi-k2.6:cloud
+ qwen3.5:cloud
+ glm-5.2:cloud
+ glm-5.1:cloud
+ gpt-oss:120b-cloud
+ minimax-m3:cloud
+ minimax-m2.7:cloud))
+
+ (setopt gptel-backend (gptel-get-backend "Ollama Cloud")
+ gptel-model 'kimi-k2.7-code:cloud)
+
+ ;; Tools: web access via Ollama Cloud's web API, plus filesystem/buffer/
+ ;; command access. Only `run_command' and `obscura_scrape' confirm first.
+
+ (gptel-make-tool
+ :name "web_search"
+ :category "web"
+ :description (concat "Search the web and return a list of results, each with a "
+ "title, URL and a content snippet. Use this for current "
+ "events or anything outside your training data, then call "
+ "`web_fetch' on a promising URL to read it in full.")
+ :args (list '(:name "query" :type string
+ :description "The search query.")
+ '(:name "max_results" :type integer :optional t
+ :description "How many results to return (1-10, default 5)."))
+ :function
+ (lambda (query &optional max_results)
+ (let* ((payload `(("query" . ,query)
+ ,@(when (and (integerp max_results) (> max_results 0))
+ `(("max_results" . ,(min max_results 10))))))
+ (results (alist-get 'results (mine/ollama-api-post "/api/web_search" payload))))
+ (if (null results)
+ "No results found."
+ (mapconcat
+ (lambda (r)
+ (format "## %s\n%s\n\n%s"
+ (alist-get 'title r) (alist-get 'url r) (alist-get 'content r)))
+ results "\n\n---\n\n")))))
+
+ (gptel-make-tool
+ :name "web_fetch"
+ :category "web"
+ :description "Fetch and return the main text content of a web page, given its URL."
+ :args (list '(:name "url" :type string
+ :description "The full URL of the page to fetch."))
+ :function
+ (lambda (url)
+ (let* ((resp (mine/ollama-api-post "/api/web_fetch" `(("url" . ,url))))
+ (content (alist-get 'content resp)))
+ (if (and (stringp content) (not (string-empty-p content)))
+ (format "# %s\n\n%s" (or (alist-get 'title resp) url) content)
+ "Could not extract any content from that URL."))))
+
+ (gptel-make-tool
+ :name "read_url"
+ :category "web"
+ :description (concat "Fetch a URL directly (no API) and return its rendered text. "
+ "A fallback for `web_fetch'.")
+ :args (list '(:name "url" :type string :description "The URL to read."))
+ :function
+ (lambda (url)
+ (with-current-buffer (url-retrieve-synchronously url t t 30)
+ (goto-char (point-min))
+ (forward-paragraph)
+ (let ((dom (libxml-parse-html-region (point) (point-max))))
+ (run-at-time 0 nil #'kill-buffer (current-buffer))
+ (with-temp-buffer
+ (shr-insert-document dom)
+ (buffer-substring-no-properties (point-min) (point-max)))))))
+
+ (gptel-make-tool
+ :name "read_buffer"
+ :category "emacs"
+ :description "Return the full contents of a live Emacs buffer."
+ :args (list '(:name "buffer" :type string
+ :description "The name of the buffer to read."))
+ :function
+ (lambda (buffer)
+ (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)))))
+
+ (gptel-make-tool
+ :name "read_file"
+ :category "filesystem"
+ :description "Read and return the contents of a file on disk."
+ :args (list '(:name "filepath" :type string
+ :description "Path to the file. Supports relative paths and ~."))
+ :function
+ (lambda (filepath)
+ (with-temp-buffer
+ (insert-file-contents (expand-file-name filepath))
+ (buffer-string))))
+
+ (gptel-make-tool
+ :name "list_directory"
+ :category "filesystem"
+ :description "List the entries in a directory."
+ :args (list '(:name "directory" :type string
+ :description "The path to the directory to list."))
+ :function
+ (lambda (directory)
+ (mapconcat #'identity (directory-files (expand-file-name directory)) "\n")))
+
+ (gptel-make-tool
+ :name "run_command"
+ :category "command"
+ :description "Execute a shell command and return its combined output."
+ :args (list '(:name "command" :type string
+ :description "The complete shell command to run.")
+ '(:name "working_dir" :type string :optional t
+ :description "Directory to run in (default: current)."))
+ :function
+ (lambda (command &optional working_dir)
+ (with-temp-message (format "Executing: %s" command)
+ (let ((default-directory
+ (if (and working_dir (not (string-empty-p working_dir)))
+ (expand-file-name working_dir)
+ default-directory)))
+ (shell-command-to-string command))))
+ :confirm t
+ :include t)
+
+ (gptel-make-tool
+ :name "obscura_scrape"
+ :category "web"
+ :description (concat "Scrape a web page with the Obscura headless browser "
+ "(real JS rendering, stealth). Use this instead of "
+ "`web_fetch'/`read_url' for JavaScript-heavy or "
+ "bot-protected pages. Returns the page as markdown by "
+ "default; other formats give raw html, plain text, or a "
+ "list of links.")
+ :args (list '(:name "url" :type string
+ :description "The URL to scrape.")
+ '(:name "format" :type string :optional t
+ :description "Output: markdown (default), text, html, or links.")
+ '(:name "selector" :type string :optional t
+ :description "Optional CSS selector to wait for before extracting."))
+ :function
+ (lambda (url &optional format selector)
+ (if-let* ((exe (executable-find "obscura")))
+ (with-temp-message (format "Obscura scraping %s" url)
+ (with-output-to-string
+ (apply #'call-process exe nil (list standard-output nil) nil
+ "fetch" url
+ "--dump" (if (member format '("text" "html" "links" "markdown"))
+ format "markdown")
+ "--wait-until" "networkidle0"
+ (when (and selector (not (string-empty-p selector)))
+ (list "--selector" selector)))))
+ "Error: the `obscura' binary is not on PATH. Install it (https://github.com/h4ckf0r0day/obscura) or use `web_fetch'/`read_url' instead."))
+ :confirm t
+ :include t)
+
+ (setq gptel-tools
+ (append (gptel-get-tool "web")
+ (gptel-get-tool "emacs")
+ (gptel-get-tool "filesystem")
+ (gptel-get-tool "command")))
+
+ ;; Presets: apply with `C-c L p' or an `@name' cookie at the start of a prompt.
+
+ (gptel-make-preset 'research
+ :description "Web research: search, fetch and scrape, then synthesise."
+ :system (concat "You are a research assistant working inside Emacs. Use the "
+ "web tools to gather up-to-date information before answering: "
+ "`web_search' to find sources, `web_fetch'/`read_url' to read "
+ "them, and `obscura_scrape' for JavaScript-heavy or protected "
+ "pages. Always cite the URLs you used. Be concise.")
+ :tools '("web_search" "web_fetch" "read_url" "obscura_scrape")
+ :confirm-tool-calls nil)
+
+ (gptel-make-preset 'agent
+ :description "Coding agent: read files/buffers and run commands in Emacs."
+ :system (concat "You are a careful coding agent operating inside Emacs. Use "
+ "the filesystem and buffer tools to inspect the project, and "
+ "`run_command' to build, test or inspect the repo. Explain "
+ "what you will do, then act. Prefer minimal, surgical changes.")
+ :tools '("read_file" "read_buffer" "list_directory" "run_command")
+ :confirm-tool-calls t)
+
+ (gptel-make-preset 'full
+ :description "Everything: web + filesystem + Emacs + commands."
+ :system 'default
+ :tools '("web_search" "web_fetch" "read_url" "obscura_scrape"
+ "read_file" "read_buffer" "list_directory" "run_command"))
+
+ (gptel-make-preset 'chat
+ :description "Plain conversation, no tools."
+ :system 'chat
+ :tools nil
+ :use-tools nil)
+
+ ;; One preset per model: `@qwen', `@glm', ... switch model for one prompt.
+ (dolist (spec '((kimi-code . kimi-k2.7-code:cloud)
+ (kimi . kimi-k2.6:cloud)
+ (qwen . qwen3.5:cloud)
+ (glm . glm-5.2:cloud)
+ (glm51 . glm-5.1:cloud)
+ (gpt-oss . gpt-oss:120b-cloud)
+ (minimax . minimax-m3:cloud)
+ (minimax2 . minimax-m2.7:cloud)))
+ (gptel-make-preset (car spec)
+ :description (format "Use the %s model on Ollama Cloud." (cdr spec))
+ :backend "Ollama Cloud"
+ :model (cdr spec))))
+
+ ;; Bundled with gptel (same repo, already on `load-path'); no recipe needed.
+ (use-package gptel-transient
+ :after gptel
+ :commands (gptel-menu gptel-system-prompt gptel-tools))
+#+end_src
+
+* Literate programming
+
+Tools for working with infrastructure and configuration files in a literate
+way: Dockerfiles, Compose files, shell scripts, REST clients, and org-babel
+sources so we can tangle and run code blocks from this very file.
+
+** Languages and file modes
+
+#+begin_src emacs-lisp
+ (use-package dockerfile-mode
+ :straight t
+ :mode (("\\.Dockerfile\\'" . dockerfile-mode)
+ ("/Dockerfile\\'" . dockerfile-mode)))
+
+ (use-package docker
+ :straight t
+ :commands (docker docker-compose))
+
+ (use-package nginx-mode
+ :straight t
+ :mode "\\.nginx\\'")
+
+ (use-package toml-mode
+ :straight t
+ :mode "\\.toml\\'")
+
+ (use-package ini-mode
+ :straight t
+ :mode (("\\.ini\\'" . ini-mode)
+ ("/\\.env\\.\\(?:local\\|development\\|production\\)?\\'" . ini-mode)))
+
+ (use-package restclient
+ :straight t
+ :mode (("\\.http\\'" . restclient-mode)
+ ("\\.rest\\'" . restclient-mode)))
+
+ (use-package graphql-mode
+ :straight t
+ :mode "\\.gql\\(?:s\\)?\\'")
+
+ (use-package docker-compose-mode
+ :straight t
+ :mode (("docker-compose\\(?:\\..*\\)?\\.ya?ml\\'" . docker-compose-mode)
+ ("compose\\.ya?ml\\'" . docker-compose-mode)))
+
+#+end_src
+
+** Org-babel languages
+
+These give us executable Org source blocks for shell scripts, REST calls,
+and inline LLM completions. `ob-async' lets long blocks (model downloads,
+container pulls) run in the background.
+
+#+begin_src emacs-lisp
+ (use-package ob-async
+ :straight t
+ :after org)
+
+ (use-package ob-restclient
+ :straight t
+ :after org)
+
+ (use-package ob-http
+ :straight t
+ :after org)
+
+ (use-package ob-graphql
+ :straight t
+ :after org)
+
+ (use-package ob-mermaid
+ :straight t
+ :after org
+ :config
+ (setopt ob-mermaid-cli-path (or (executable-find "mmdc") "npx")))
+
+ (use-package ob-d2
+ :straight t
+ :after org)
+
+ (use-package ob-llm
+ :straight t
+ :after org
+ :config
+ ;; Send babel blocks through gptel rather than a hosted API directly.
+ (setopt ob-llm-backend #'gptel-request
+ ob-llm-stream t))
+
+ (use-package ob-tmux
+ :straight t
+ :after org
+ :config
+ (setopt ob-tmux-location-alist
+ '(("default" . "/tmp")
+ ("work" . "/var/tmp"))))
+#+end_src
+
+Enable a useful default set of babel languages and configure evaluation
+safety so we can run trusted devops blocks without per-block confirmation.
+
+#+begin_src emacs-lisp
+ (with-eval-after-load 'org
+ (setopt org-babel-default-languages
+ '(emacs-lisp shell python docker dockerfile restclient http graphql mermaid d2 tmux llm))
+ (setopt org-confirm-babel-evaluate nil
+ org-babel-default-header-args:shell '((:results . "output") (:async . "yes"))
+ org-babel-default-header-args:tmux '((:results . "output") (:async . "yes"))
+ org-babel-default-header-args:restclient '((:results . "raw"))
+ org-babel-default-header-args:http '((:results . "raw"))))
+#+end_src
blob - fc0d6353b586367a4b8757c52c851e55e6dbd7dc
blob + 818c440582ccc4c765cfb3a11da03af6330409bb
--- init.el
+++ init.el
(error "Emacs configuration only works with Emacs 29 and newer; you have version %s"
emacs-major-version))
-(setopt initial-major-mode 'fundamental-mode)
-(setopt inhibit-startup-screen t)
-(setopt initial-scratch-message nil)
-(setopt display-time-default-load-average nil)
+(setopt initial-major-mode 'fundamental-mode
+ inhibit-startup-screen t
+ initial-scratch-message nil
+ display-time-default-load-average nil
+ sentence-end-double-space nil
+ use-short-answers t)
-(setopt auto-revert-avoid-polling t)
-(setopt auto-revert-interval 5)
-(setopt auto-revert-check-vc-info t)
+(setopt auto-revert-avoid-polling t
+ auto-revert-interval 5
+ auto-revert-check-vc-info t)
(global-auto-revert-mode 1)
(savehist-mode 1)
(windmove-default-keybindings 'control)
-(setopt sentence-end-double-space nil)
-
(when (display-graphic-p)
(context-menu-mode -1)
(tool-bar-mode -1)
(menu-bar-mode -1)
- (scroll-bar-mode -1)
- (set-frame-parameter nil 'font "Go Mono-14"))
+ (scroll-bar-mode -1))
+;; Use Go Mono everywhere. `default' is set directly here; `fixed-pitch'
+;; and `variable-pitch' must be pointed at it explicitly so that e.g.
+;; `variable-pitch-mode' in org-mode still renders in Go Mono.
+(set-face-attribute 'default nil :family "Go Mono" :height 140)
+(set-face-attribute 'fixed-pitch nil :family "Go Mono" :height 1.0)
+(set-face-attribute 'variable-pitch nil :family "Go Mono" :height 1.0)
+
(unless (display-graphic-p)
(xterm-mouse-mode 1))
(cua-mode 1)
-(setopt display-line-numbers-width 3)
+(keymap-set global-map "C-x k" #'kill-current-buffer)
+(add-hook 'before-save-hook #'whitespace-cleanup)
+
+(setopt display-line-numbers-width 3
+ tab-bar-show 1)
(add-hook 'prog-mode-hook #'display-line-numbers-mode)
(add-hook 'text-mode-hook #'visual-line-mode)
(dolist (hook '(text-mode-hook prog-mode-hook))
(add-hook hook #'hl-line-mode))
-(setopt tab-bar-show 1)
-
-;; Start the Emacs server if it is not already running so `emacsclient' can
-;; attach to this session. Remove the server socket when this Emacs exits.
(require 'server)
(unless (server-running-p)
(server-start))
(lock-dir (expand-file-name "locks/" "~/.cache/emacs")))
(make-directory auto-save-dir t)
(make-directory lock-dir t)
- (setopt auto-save-file-name-transforms `((".*" ,auto-save-dir t)))
- (setopt lock-file-name-transforms `((".*" ,lock-dir t))))
+ (setopt auto-save-file-name-transforms `((".*" ,auto-save-dir t))
+ lock-file-name-transforms `((".*" ,lock-dir t))))
(use-package modus-themes
:straight t
:defer 1
:config (which-key-mode))
+(use-package smart-mode-line
+ :straight t
+ :demand t
+ :custom
+ (sml/no-confirm-load-theme t)
+ (sml/theme 'respectful)
+ :config
+ (sml/setup))
+
+(use-package minions
+ :straight t
+ :demand t
+ :config
+ (minions-mode))
+
+;; Lets `:diminish' hide minor-mode lighters from the mode line.
+(use-package diminish
+ :straight t
+ :demand t)
+
(add-to-list 'tab-bar-format 'tab-bar-format-align-right 'append)
(add-to-list 'tab-bar-format 'tab-bar-format-global 'append)
-(setopt display-time-format "%a %F %T")
-(setopt display-time-interval 1)
+(setopt display-time-format "%a %F %T"
+ display-time-interval 1)
(display-time-mode 1)
-(setopt mouse-wheel-tilt-scroll t)
-(setopt mouse-wheel-flip-direction t)
-(setopt mouse-wheel-scroll-amount '(1 ((shift) . 5) ((control) . 0)))
-(setopt mouse-wheel-progressive-speed nil)
+(setopt mouse-wheel-tilt-scroll t
+ mouse-wheel-flip-direction t
+ mouse-wheel-scroll-amount '(1 ((shift) . 5) ((control) . 0))
+ mouse-wheel-progressive-speed nil)
+(setopt scroll-margin 4
+ scroll-conservatively 101
+ scroll-up-aggressively 0.01
+ scroll-down-aggressively 0.01
+ auto-window-vscroll nil)
+
(blink-cursor-mode -1)
(pixel-scroll-precision-mode 1)
-(setopt scroll-margin 4)
-(setopt scroll-conservatively 101)
-(setopt scroll-up-aggressively 0.01)
-(setopt scroll-down-aggressively 0.01)
-(setopt auto-window-vscroll nil)
(use-package lin
:straight t
:straight (circadian :type git :host github :repo "guidoschmidt/circadian.el")
:demand t
:config
- (setopt calendar-latitude 48.1386)
- (setopt calendar-longitude 17.1053)
- (setopt circadian-themes '((:sunrise . ef-orange)
+ (setopt calendar-latitude 48.1386
+ calendar-longitude 17.1053
+ circadian-themes '((:sunrise . ef-orange)
(:sunset . modus-vivendi)))
(circadian-setup))
-(setopt completion-cycle-threshold 3)
-(setopt tab-always-indent 'complete)
+(setopt completion-cycle-threshold 3
+ tab-always-indent 'complete)
(keymap-set minibuffer-mode-map "TAB" #'minibuffer-complete)
:straight t
:after eshell
:custom
- (eat-term-name "xterm")
+ ;; A universally-installed TERM, so shells eat spawns never look up the
+ ;; bundled `eat-truecolor' entry (which `tput' fails to find otherwise).
+ (eat-term-name "xterm-256color")
:config
(eat-eshell-mode)
(eat-eshell-visual-command-mode))
+(use-package tramp
+ :straight (:type built-in)
+ :defer t
+ :custom
+ (tramp-default-method "ssh")
+ (tramp-verbose 2)
+ (remote-file-name-inhibit-cache 60)
+ :config
+ (add-to-list 'tramp-remote-path 'tramp-own-remote-path))
+
(use-package treesit-auto
:straight t
:demand t
(eglot-extend-to-xref t)
:config (fset #'jsonrpc--log-event #'ignore))
+;; GC and the file-name handler are restored on `emacs-startup-hook'
+;; (see early-init). Larger process reads make eglot/LSP feel snappier.
+(setopt read-process-output-max (* 4 1024 1024)
+ process-adaptive-read-buffering nil)
+
(use-package tempel
:straight t
:bind (("M-*" . tempel-insert)
(keymap-set denote-prefix-map "t" #'org-agenda-list)
(keymap-set denote-prefix-map "c" #'org-capture)
+(defvar-keymap gptel-prefix-map
+ :doc "Prefix map for gptel commands.")
+(keymap-set global-map "C-c L" gptel-prefix-map)
+(keymap-set gptel-prefix-map "s" #'gptel-send)
+(keymap-set gptel-prefix-map "m" #'gptel-menu)
+(keymap-set gptel-prefix-map "r" #'gptel-rewrite)
+(keymap-set gptel-prefix-map "a" #'gptel-add)
+(keymap-set gptel-prefix-map "A" #'gptel-add-file)
+(keymap-set gptel-prefix-map "p" #'gptel-preset)
+(keymap-set gptel-prefix-map "t" #'gptel-tools)
+(keymap-set gptel-prefix-map "n" #'mine/gptel-denote-session)
+
+(defvar-keymap devops-prefix-map
+ :doc "Prefix map for devops / container / shell commands.")
+(keymap-set global-map "C-c d" devops-prefix-map)
+(keymap-set devops-prefix-map "d" #'docker)
+(keymap-set devops-prefix-map "c" #'docker-compose)
+(keymap-set devops-prefix-map "s" #'shell-command)
+(keymap-set devops-prefix-map "b" #'org-babel-execute-buffer)
+(keymap-set devops-prefix-map "B" #'org-babel-execute-subtree)
+(keymap-set devops-prefix-map "t" #'ob-tmux-open)
+
(with-eval-after-load 'corfu
(keymap-set corfu-map "C-b" #'corfu-scroll-down)
(keymap-set corfu-map "C-f" #'corfu-scroll-up)
("C-c t k" . tmr-remove)
("C-c t s" . tmr-stop)))
-;; GC and the file-name handler are restored on `emacs-startup-hook'
-;; (see early-init). Larger process reads make eglot/LSP feel snappier.
-(setopt read-process-output-max (* 4 1024 1024)
- process-adaptive-read-buffering nil)
+(defun mine/gptel-denote-session ()
+ "Create a Denote note and start a gptel session in it."
+ (interactive)
+ (let ((title (read-string "GPTel session title: ")))
+ (denote title '("ai" "gptel"))
+ (gptel-mode)))
+
+ (defun mine/gptel-auto-save (_beg _end)
+ "Save current gptel buffer after each response, if it is a Denote note.
+Called from `gptel-post-response-functions', which passes the
+response beginning and end positions; we ignore both."
+ (when (and buffer-file-name
+ (derived-mode-p 'org-mode)
+ (string-prefix-p (expand-file-name denote-directory)
+ (file-name-directory buffer-file-name)))
+ (save-buffer)))
+
+ (add-hook 'gptel-post-response-functions #'mine/gptel-auto-save)
+
+ ;; --- Ollama Cloud REST helpers (shared by the gptel web tools) -------
+
+ (defun mine/ollama-api-key ()
+ "Return the Ollama Cloud API key stored in auth-source.
+Expects a line in `auth-sources' for host ollama.com, user apikey."
+ (or (auth-source-pick-first-password :host "ollama.com" :user "apikey")
+ (user-error "No Ollama API key in auth-source for host ollama.com")))
+
+ (defun mine/ollama-api-post (path payload)
+ "POST PAYLOAD (an alist) as JSON to Ollama API PATH, return parsed JSON.
+Signals an error on a non-2xx response. Runs synchronously."
+ (let* ((url-request-method "POST")
+ (url-request-extra-headers
+ `(("Content-Type" . "application/json")
+ ("Authorization" . ,(concat "Bearer " (mine/ollama-api-key)))))
+ (url-request-data (encode-coding-string (json-encode payload) 'utf-8))
+ (buf (url-retrieve-synchronously (concat "https://ollama.com" path)
+ 'silent nil 60)))
+ (unless buf (error "Ollama API request to %s timed out" path))
+ (unwind-protect
+ (with-current-buffer buf
+ (goto-char (point-min))
+ (let ((status (and (re-search-forward "^HTTP/[0-9.]+ \\([0-9]+\\)" nil t)
+ (string-to-number (match-string 1)))))
+ (unless (re-search-forward "\n\n" nil t)
+ (error "Malformed response from Ollama API"))
+ (let* ((json-object-type 'alist)
+ (json-array-type 'list)
+ (json-key-type 'symbol)
+ (body (json-read)))
+ (when (and status (>= status 400))
+ (error "Ollama API %s failed (HTTP %d): %s" path status
+ (or (alist-get 'error body) body)))
+ body)))
+ (kill-buffer buf))))
+
+(use-package gptel
+ :straight (gptel :type git :host github :repo "karthink/gptel")
+ :demand t
+ :bind (:map gptel-mode-map
+ ("C-c RET" . gptel-send)
+ ("C-c C-r" . gptel-rewrite)
+ ("C-c C-a" . gptel-add))
+ :custom
+ (gptel-use-curl t)
+ (gptel-stream t)
+ (gptel-default-mode 'org-mode)
+ (gptel-use-header-line t)
+ (gptel-org-ignore-elements '(property-drawer))
+ (gptel-directives
+ '((default . "You are a large language model living in Emacs and a helpful assistant. Respond concisely.")
+ (programming . "You are a careful programmer working from inside Emacs. Provide code, with minimal surrounding prose.")
+ (reviewing . "You are reviewing code or text in Emacs. Be terse, point out concrete issues, and suggest fixes.")
+ (literate . "You are helping with a literate configuration in an Org file. Prefer Org markup and explain only what is non-obvious.")
+ (chat . "You are a conversation partner. Respond concisely.")))
+ (gptel-use-tools t)
+ (gptel-confirm-tool-calls 'auto)
+ (gptel-include-tool-results 'auto)
+ :config
+ ;; `gptel-ollama' is loaded only so `ob-llm' keeps working; the backend
+ ;; itself is the OpenAI-compatible Ollama Cloud endpoint below.
+ (require 'gptel-ollama)
+ (require 'gptel-openai)
+ (gptel-make-openai "Ollama Cloud"
+ :host "ollama.com"
+ :protocol "https"
+ :endpoint "/v1/chat/completions"
+ :stream t
+ :key (lambda ()
+ (auth-source-pick-first-password
+ :host "ollama.com" :user "apikey"))
+ :models '(kimi-k2.7-code:cloud
+ kimi-k2.6:cloud
+ qwen3.5:cloud
+ glm-5.2:cloud
+ glm-5.1:cloud
+ gpt-oss:120b-cloud
+ minimax-m3:cloud
+ minimax-m2.7:cloud))
+
+ (setopt gptel-backend (gptel-get-backend "Ollama Cloud")
+ gptel-model 'kimi-k2.7-code:cloud)
+
+ ;; Tools: web access via Ollama Cloud's web API, plus filesystem/buffer/
+ ;; command access. Only `run_command' and `obscura_scrape' confirm first.
+
+ (gptel-make-tool
+ :name "web_search"
+ :category "web"
+ :description (concat "Search the web and return a list of results, each with a "
+ "title, URL and a content snippet. Use this for current "
+ "events or anything outside your training data, then call "
+ "`web_fetch' on a promising URL to read it in full.")
+ :args (list '(:name "query" :type string
+ :description "The search query.")
+ '(:name "max_results" :type integer :optional t
+ :description "How many results to return (1-10, default 5)."))
+ :function
+ (lambda (query &optional max_results)
+ (let* ((payload `(("query" . ,query)
+ ,@(when (and (integerp max_results) (> max_results 0))
+ `(("max_results" . ,(min max_results 10))))))
+ (results (alist-get 'results (mine/ollama-api-post "/api/web_search" payload))))
+ (if (null results)
+ "No results found."
+ (mapconcat
+ (lambda (r)
+ (format "## %s\n%s\n\n%s"
+ (alist-get 'title r) (alist-get 'url r) (alist-get 'content r)))
+ results "\n\n---\n\n")))))
+
+ (gptel-make-tool
+ :name "web_fetch"
+ :category "web"
+ :description "Fetch and return the main text content of a web page, given its URL."
+ :args (list '(:name "url" :type string
+ :description "The full URL of the page to fetch."))
+ :function
+ (lambda (url)
+ (let* ((resp (mine/ollama-api-post "/api/web_fetch" `(("url" . ,url))))
+ (content (alist-get 'content resp)))
+ (if (and (stringp content) (not (string-empty-p content)))
+ (format "# %s\n\n%s" (or (alist-get 'title resp) url) content)
+ "Could not extract any content from that URL."))))
+
+ (gptel-make-tool
+ :name "read_url"
+ :category "web"
+ :description (concat "Fetch a URL directly (no API) and return its rendered text. "
+ "A fallback for `web_fetch'.")
+ :args (list '(:name "url" :type string :description "The URL to read."))
+ :function
+ (lambda (url)
+ (with-current-buffer (url-retrieve-synchronously url t t 30)
+ (goto-char (point-min))
+ (forward-paragraph)
+ (let ((dom (libxml-parse-html-region (point) (point-max))))
+ (run-at-time 0 nil #'kill-buffer (current-buffer))
+ (with-temp-buffer
+ (shr-insert-document dom)
+ (buffer-substring-no-properties (point-min) (point-max)))))))
+
+ (gptel-make-tool
+ :name "read_buffer"
+ :category "emacs"
+ :description "Return the full contents of a live Emacs buffer."
+ :args (list '(:name "buffer" :type string
+ :description "The name of the buffer to read."))
+ :function
+ (lambda (buffer)
+ (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)))))
+
+ (gptel-make-tool
+ :name "read_file"
+ :category "filesystem"
+ :description "Read and return the contents of a file on disk."
+ :args (list '(:name "filepath" :type string
+ :description "Path to the file. Supports relative paths and ~."))
+ :function
+ (lambda (filepath)
+ (with-temp-buffer
+ (insert-file-contents (expand-file-name filepath))
+ (buffer-string))))
+
+ (gptel-make-tool
+ :name "list_directory"
+ :category "filesystem"
+ :description "List the entries in a directory."
+ :args (list '(:name "directory" :type string
+ :description "The path to the directory to list."))
+ :function
+ (lambda (directory)
+ (mapconcat #'identity (directory-files (expand-file-name directory)) "\n")))
+
+ (gptel-make-tool
+ :name "run_command"
+ :category "command"
+ :description "Execute a shell command and return its combined output."
+ :args (list '(:name "command" :type string
+ :description "The complete shell command to run.")
+ '(:name "working_dir" :type string :optional t
+ :description "Directory to run in (default: current)."))
+ :function
+ (lambda (command &optional working_dir)
+ (with-temp-message (format "Executing: %s" command)
+ (let ((default-directory
+ (if (and working_dir (not (string-empty-p working_dir)))
+ (expand-file-name working_dir)
+ default-directory)))
+ (shell-command-to-string command))))
+ :confirm t
+ :include t)
+
+ (gptel-make-tool
+ :name "obscura_scrape"
+ :category "web"
+ :description (concat "Scrape a web page with the Obscura headless browser "
+ "(real JS rendering, stealth). Use this instead of "
+ "`web_fetch'/`read_url' for JavaScript-heavy or "
+ "bot-protected pages. Returns the page as markdown by "
+ "default; other formats give raw html, plain text, or a "
+ "list of links.")
+ :args (list '(:name "url" :type string
+ :description "The URL to scrape.")
+ '(:name "format" :type string :optional t
+ :description "Output: markdown (default), text, html, or links.")
+ '(:name "selector" :type string :optional t
+ :description "Optional CSS selector to wait for before extracting."))
+ :function
+ (lambda (url &optional format selector)
+ (if-let* ((exe (executable-find "obscura")))
+ (with-temp-message (format "Obscura scraping %s" url)
+ (with-output-to-string
+ (apply #'call-process exe nil (list standard-output nil) nil
+ "fetch" url
+ "--dump" (if (member format '("text" "html" "links" "markdown"))
+ format "markdown")
+ "--wait-until" "networkidle0"
+ (when (and selector (not (string-empty-p selector)))
+ (list "--selector" selector)))))
+ "Error: the `obscura' binary is not on PATH. Install it (https://github.com/h4ckf0r0day/obscura) or use `web_fetch'/`read_url' instead."))
+ :confirm t
+ :include t)
+
+ (setq gptel-tools
+ (append (gptel-get-tool "web")
+ (gptel-get-tool "emacs")
+ (gptel-get-tool "filesystem")
+ (gptel-get-tool "command")))
+
+ ;; Presets: apply with `C-c L p' or an `@name' cookie at the start of a prompt.
+
+ (gptel-make-preset 'research
+ :description "Web research: search, fetch and scrape, then synthesise."
+ :system (concat "You are a research assistant working inside Emacs. Use the "
+ "web tools to gather up-to-date information before answering: "
+ "`web_search' to find sources, `web_fetch'/`read_url' to read "
+ "them, and `obscura_scrape' for JavaScript-heavy or protected "
+ "pages. Always cite the URLs you used. Be concise.")
+ :tools '("web_search" "web_fetch" "read_url" "obscura_scrape")
+ :confirm-tool-calls nil)
+
+ (gptel-make-preset 'agent
+ :description "Coding agent: read files/buffers and run commands in Emacs."
+ :system (concat "You are a careful coding agent operating inside Emacs. Use "
+ "the filesystem and buffer tools to inspect the project, and "
+ "`run_command' to build, test or inspect the repo. Explain "
+ "what you will do, then act. Prefer minimal, surgical changes.")
+ :tools '("read_file" "read_buffer" "list_directory" "run_command")
+ :confirm-tool-calls t)
+
+ (gptel-make-preset 'full
+ :description "Everything: web + filesystem + Emacs + commands."
+ :system 'default
+ :tools '("web_search" "web_fetch" "read_url" "obscura_scrape"
+ "read_file" "read_buffer" "list_directory" "run_command"))
+
+ (gptel-make-preset 'chat
+ :description "Plain conversation, no tools."
+ :system 'chat
+ :tools nil
+ :use-tools nil)
+
+ ;; One preset per model: `@qwen', `@glm', ... switch model for one prompt.
+ (dolist (spec '((kimi-code . kimi-k2.7-code:cloud)
+ (kimi . kimi-k2.6:cloud)
+ (qwen . qwen3.5:cloud)
+ (glm . glm-5.2:cloud)
+ (glm51 . glm-5.1:cloud)
+ (gpt-oss . gpt-oss:120b-cloud)
+ (minimax . minimax-m3:cloud)
+ (minimax2 . minimax-m2.7:cloud)))
+ (gptel-make-preset (car spec)
+ :description (format "Use the %s model on Ollama Cloud." (cdr spec))
+ :backend "Ollama Cloud"
+ :model (cdr spec))))
+
+;; Bundled with gptel (same repo, already on `load-path'); no recipe needed.
+(use-package gptel-transient
+ :after gptel
+ :commands (gptel-menu gptel-system-prompt gptel-tools))
+
+(use-package dockerfile-mode
+ :straight t
+ :mode (("\\.Dockerfile\\'" . dockerfile-mode)
+ ("/Dockerfile\\'" . dockerfile-mode)))
+
+(use-package docker
+ :straight t
+ :commands (docker docker-compose))
+
+(use-package nginx-mode
+ :straight t
+ :mode "\\.nginx\\'")
+
+(use-package toml-mode
+ :straight t
+ :mode "\\.toml\\'")
+
+(use-package ini-mode
+ :straight t
+ :mode (("\\.ini\\'" . ini-mode)
+ ("/\\.env\\.\\(?:local\\|development\\|production\\)?\\'" . ini-mode)))
+
+(use-package restclient
+ :straight t
+ :mode (("\\.http\\'" . restclient-mode)
+ ("\\.rest\\'" . restclient-mode)))
+
+(use-package graphql-mode
+ :straight t
+ :mode "\\.gql\\(?:s\\)?\\'")
+
+(use-package docker-compose-mode
+ :straight t
+ :mode (("docker-compose\\(?:\\..*\\)?\\.ya?ml\\'" . docker-compose-mode)
+ ("compose\\.ya?ml\\'" . docker-compose-mode)))
+
+(use-package ob-async
+ :straight t
+ :after org)
+
+(use-package ob-restclient
+ :straight t
+ :after org)
+
+(use-package ob-http
+ :straight t
+ :after org)
+
+(use-package ob-graphql
+ :straight t
+ :after org)
+
+(use-package ob-mermaid
+ :straight t
+ :after org
+ :config
+ (setopt ob-mermaid-cli-path (or (executable-find "mmdc") "npx")))
+
+(use-package ob-d2
+ :straight t
+ :after org)
+
+(use-package ob-llm
+ :straight t
+ :after org
+ :config
+ ;; Send babel blocks through gptel rather than a hosted API directly.
+ (setopt ob-llm-backend #'gptel-request
+ ob-llm-stream t))
+
+(use-package ob-tmux
+ :straight t
+ :after org
+ :config
+ (setopt ob-tmux-location-alist
+ '(("default" . "/tmp")
+ ("work" . "/var/tmp"))))
+
+(with-eval-after-load 'org
+ (setopt org-babel-default-languages
+ '(emacs-lisp shell python docker dockerfile restclient http graphql mermaid d2 tmux llm))
+ (setopt org-confirm-babel-evaluate nil
+ org-babel-default-header-args:shell '((:results . "output") (:async . "yes"))
+ org-babel-default-header-args:tmux '((:results . "output") (:async . "yes"))
+ org-babel-default-header-args:restclient '((:results . "raw"))
+ org-babel-default-header-args:http '((:results . "raw"))))