Evolving a Modular dev Experience in Emacs
Foreword
Prompted by projects like zuckerman and Mario's fantastic article on growing his own agent loop, this felt like a good moment to share another way of interacting with LLMs.
Although there is a lot of Emacs-centric setting-the-scene, I'm hoping people don't take this as a setup guide or a pitch to switch to Emacs.
Consider it a nudge toward building small, self-owned tools. Instead of the flavor of the week, or an opaque company-controlled harness, you can assemble a cozy backyard shed to tinker about in and keep under your control.
Gptel
Even before Claude Code, my on-ramp to this workflow was experimenting with the gptel package for Emacs. The package's default interaction model is intentionally minimal, and fits cleanly into Emacs’s text-over-everything lineage.
Instead of a separate chat UI, any buffer becomes an interface.
On the first gptel-send, everything above point is treated as the prompt, and the model’s response is inserted directly below. Behind the scenes, gptel tags responses with metadata so subsequent sends can reconstruct message boundaries, distinguish user from assistant turns, and serialize each segment with the correct role.
This feels native in Emacs, where:
- text is the interface
- buffers are data
- Emacs Lisp is the glue
Documents as Conversation
The conversation remains a plain Org or Markdown document. Because of this, a new interaction model arises:
- You can scroll, edit, reorder, and annotate freely.
- You can save snapshots at any point (just save another file).
- You can treat the transcript as working material, not a fixed chat log.
Even without external tools, this already enables workflows that differ from the standard chat interfaces:
Context Management as Document Hygiene
In this model, prompt engineering is mostly document editing.
- Deleting paragraphs is context pruning.
- Reordering sections changes salience.
- Converting rough notes into headings and lists creates structure.
- Keeping only accepted decisions turns the buffer into a running spec.
For example, I often build a document exploring possible approaches to a problem, have the LLM summarize the chosen direction, then manually trim the buffer down to compact the relevant context before continuing.
Iteration is cheap, and the focus becomes on keeping a clean working document that the model (and you) can follow and read.
Forking
Branching conversations become simple.
If a conversational path stops yielding useful output
- Reposition your cursor at the point you want to branch.
- Add instructions & remove offending context
- re-trigger
gptel-send.
Everything below the cursor (in the undesired path) is ignored.
Honing The Shed
After a few months of copy-pasting code and manually building context, Model Context Protocol was hot-off-the-press and being discussed as a topic on the gptel issue tracker. Reading through that thread led me to discover that gptel itself had added native tool support as a parallel evolution to Anthropic's offering.
Tools in gptel
Gptel tools are defined in Elisp and consist of a description, a schema and the resulting function to be called. The resulting text returned by the function is included back into the conversation context.
Since they are defined in Elisp, the associated functions also have access to the full Emacs environment.
Example Tool
As an example, below is the Elisp defining a tool to look for the binding of a specific key combination.
Tool Definition
(gptel-make-tool
:function (lambda (keys)
(let ((cmd (key-binding (kbd keys))))
(if cmd
(format "%s runs `%s'\n\n%s"
keys cmd
(or (documentation cmd) "No documentation."))
"No binding found.")))
:name "describe_key"
:category "emacs"
:description "Show what command a key sequence runs and its documentation"
:args (list '(:name "keys" :type "string" :description "Emacs key sequence, e.g. C-x C-f")))
Example Use
Tool Calldescribe_keykeys SPC b b
▸
| keys | SPC b b |
SPC b b runs `+vertico/switch-workspace-buffer' Switch to another buffer in the same or a specified workspace. Type the workspace's number (starting from 1) followed by a space to display its buffer list. Selecting a buffer in another workspace will switch to that workspace instead. If FORCE-SAME-WORKSPACE (the prefix arg) is non-nil, that buffer will be opened in the current workspace instead. (fn &optional FORCE-SAME-WORKSPACE)
Settings and presets

Once you have the basics, the surface area of gptel expands. You can:
- Set system prompts
- Switch providers and models
- Define tool calls using elisp
With the stage set, what follows is a dive into the tools and presets I've found useful, not as replacements for Claude Code or Codex, but rather as a different mode of interaction that can be very satisfying to work with in a semi-hands-on manner.
Philosophy of tools
The tools I've ended up with can be grouped into 4 overarching "modes" of interaction. Using these together allows us to:
- Build up a meaningful context for the problem at hand
- Affect meaningful changes.
| Category | Use Case |
|---|---|
| Discovery | Orienting the conversation within the environment |
| Retrieval | bringing in actual context |
| Research | Accessing external sources to add to the context |
| Action | Side effects on the environment |
Alongside that, there is another class of tools - meta - which I will discuss below.
Discovery tooling
get_directory_tree
Provides a structural view of a folder, with configurable depth and hidden file visibility by shelling out to the tree command. Gives the model a "mental map" of how the codebase is organized.
Source
Helper Function
(defun my/get-directory-tree (dir-path &optional max-depth show-hidden)
"Return directory tree for DIR-PATH using the `tree` command.
Expands ~ and environment variables.
MAX-DEPTH is an optional integer.
SHOW-HIDDEN is \"true\" or \"false\"."
(unless (executable-find "tree")
(error "`tree` command not found in PATH"))
(let* ((expanded (expand-file-name
(substitute-in-file-name dir-path))))
(unless (file-directory-p expanded)
(error "Not a directory: %s" expanded))
(let* ((depth-args (when (and max-depth
(integerp max-depth)
(> max-depth 0))
(list "-L" (number-to-string max-depth))))
(hidden-args (when (and show-hidden
(string= show-hidden "true"))
(list "-a")))
(args (append hidden-args
depth-args
(list expanded))))
(with-temp-buffer
(let ((exit-code (apply #'call-process "tree" nil t nil args)))
(if (zerop exit-code)
(string-trim (buffer-string))
(error "`tree` failed with exit code %d for %s"
exit-code expanded)))))))
Tool Definition
(gptel-make-tool
:name "get_directory_tree"
:function #'my/get-directory-tree
:description "Show directory tree structure with configurable depth."
:args (list '(:name "dir_path" :type string :description "Path to the directory.")
'(:name "max_depth" :type integer :description "Maximum recursion depth." :optional t)
'(:name "show_hidden" :type string :description "Show hidden files (true/false)." :optional t))
:category "discovery")
Example Interaction
Tool Callget_directory_treedir_path ~/.doom.d/local/gptel-tool-calls/ max_depth 2
▸
| dir_path | ~/.doom.d/local/gptel-tool-calls/ |
| max_depth | 2 |
gptel-tool-calls/ ├── tests/ │ ├── content-utils-test.el │ ├── file-utils-test.el │ ├── git-utils-test.el │ ├── run-tests.el │ └── test-helper.el ├── gptel-content-utils.el ├── gptel-denote-tools.el ├── gptel-elisp-utils.el ├── gptel-file-utils.el ├── gptel-git-utils.el ├── gptel-metadata-utils.el ├── gptel-patch-utils.el ├── gptel-tool-calls.el ├── gptel-vterm-utils.el └── gptel-web-utils.el
This aids the model in a few ways:
- In a well laid-out project, it has to do far less grasping in the dark when exploring for specific context.
- Instead of adding files in an arbitrary way, it has some scaffold of where to put new additions to the codebase.
- It can find extra documentation or configuration which does not follow the agent-friendly conventions.
find_buffers_and_recent
Returns a list buffers in order of most recently accessed, with optional keyword regex filtering.
Source
Helper Function
(defun my/find-buffers-and-recent (pattern)
"Find open buffers and recently opened files matching PATTERN.
PATTERN is an Emacs regex. Use empty string to list all."
(let* ((project-context (file-tools--get-project-context))
(in-project (nth 0 project-context))
(project-root (nth 1 project-context))
(pattern-provided (and pattern (not (string-empty-p pattern))))
(buffer-matches '())
(recent-matches '())
(sections '()))
;; Search open buffers
(dolist (buf (buffer-list))
(let ((buf-name (buffer-name buf))
(file-name (buffer-file-name buf)))
(when (and (not (string-prefix-p " " buf-name)) ; Skip hidden buffers
(or (not pattern-provided)
(string-match-p pattern buf-name)
(and file-name (string-match-p pattern file-name))))
(push (list buf-name file-name (buffer-modified-p buf)) buffer-matches))))
;; Search recent files
(recentf-mode 1)
(when (boundp 'recentf-list)
(dolist (file recentf-list)
(when (or (not pattern-provided)
(string-match-p pattern file)
(string-match-p pattern (file-name-nondirectory file)))
(push file recent-matches))))
;; Format results
(when buffer-matches
(let ((buffer-lines
(mapcar (lambda (item)
(file-tools--format-buffer-line
(nth 0 item) (nth 1 item) (nth 2 item)
in-project project-root))
(sort buffer-matches
(lambda (a b) (string-lessp (nth 0 a) (nth 0 b)))))))
(push (format "Open Buffers:\n%s" (string-join buffer-lines "\n")) sections)))
(when recent-matches
(let ((recent-lines
(mapcar (lambda (file-path)
(file-tools--format-file-line file-path in-project project-root))
(sort (seq-uniq recent-matches) #'string-lessp))))
(push (format "Recent Files:\n%s" (string-join recent-lines "\n")) sections)))
(if sections
(string-join (nreverse sections) "\n\n")
(format "No buffers or recent files found matching pattern '%s'" (or pattern "")))))
Tool Definition
(gptel-make-tool
:name "find_buffers_and_recent"
:function #'my/find-buffers-and-recent
:description "Find open buffers and recently opened files matching a regex pattern."
:args (list '(:name "pattern" :type string :description "Emacs regex to match against file paths."))
:category "discovery")
Example Interaction
Tool Callfind_buffers_and_recentpattern \\.el$
▸
| pattern | \\.el$ |
Open Buffers: gptel-content-utils.el (~/.doom.d/local/gptel-tool-calls/gptel-content-utils.el) gptel-tool-calls.el (~/.doom.d/local/gptel-tool-calls/gptel-tool-calls.el) Recent Files: ~/.config/emacs/.local/straight/repos/gptel/gptel-request.el ~/.doom.d/local/gptel-tool-calls/gptel-content-utils.el ~/.doom.d/local/gptel-tool-calls/gptel-file-utils.el ~/.doom.d/local/gptel-tool-calls/gptel-newpatch.el ~/.doom.d/local/gptel-tool-calls/gptel-tool-calls.el ~/.doom.d/local/gptel-tool-calls/gptel-web-utils.el
Emacs buffers are not limited to files, but may also include terminals, logs, REPLs, git diffs/logs/history buffers (by way of magit) and more. Sorting by descending interaction-date also gives us a nice signal as to what is immediately relevant.
find_subdir_files
Searches through files using regex patterns in a subdirectory. Uses fd for fast searching with sensible exclusions (.git, node_modules, etc.).
Source
Helper Function
(defun my/file-find-subdir-files (pattern root-dir)
"Find files in ROOT-DIR matching PATTERN using fd.
PATTERN is an Emacs regex. Use empty string to list all files.
ROOT-DIR can use ~ and will be expanded to an absolute path.
Returns absolute paths."
(let ((expanded-root (expand-file-name root-dir)))
(unless (file-directory-p expanded-root)
(error "Root directory '%s' is not a valid directory" expanded-root))
(let* ((pattern-provided (and pattern (not (string-empty-p pattern))))
(files '()))
(if (executable-find "fd")
;; Use fd with better filtering
(let* ((default-directory expanded-root)
(cmd-string (concat "fd --type f --absolute-path"
" --exclude .git"
" --exclude node_modules"
" --exclude .venv"
" --exclude __pycache__"
" --exclude .cache"
(if pattern-provided
(concat " " (shell-quote-argument pattern))
"")))
(output (shell-command-to-string cmd-string)))
(when (> (length (string-trim output)) 0)
(setq files (split-string (string-trim output) "\n" t))))
;; Simple fallback - convert to absolute paths
(let ((rel-files (directory-files expanded-root nil "^[^.].*")))
(setq files (mapcar (lambda (f) (expand-file-name f expanded-root))
rel-files))))
(if files
(let ((file-lines
(mapcar (lambda (file) (format " %s" file))
(sort files #'string-lessp))))
(format "Files under %s:\n%s"
(file-name-nondirectory (directory-file-name expanded-root))
(string-join file-lines "\n")))
(format "No files found matching pattern '%s' under %s"
(or pattern "") expanded-root)))))
Tool Definition
(gptel-make-tool
:name "find_subdir_files"
:function #'my/file-find-subdir-files
:description "Find files under a given directory matching a regex pattern. Uses fd if available."
:args (list
'(:name "pattern"
:type string
:description "Emacs regex to match against file paths. Use empty string to list all files.")
'(:name "root_dir"
:type string
:description "Absolute or ~-expanded path to the directory to search within."))
:category "discovery")
Example Interaction
Tool Callfind_subdir_filesroot_dir ~/src/openclaw pattern skill.*\\.md$
▸
| root_dir | ~/src/openclaw |
| pattern | skill.*\\.md$ |
Files under openclaw: ~/src/openclaw/docs/cli/skills.md ~/src/openclaw/docs/platforms/mac/skills.md ~/src/openclaw/docs/tools/creating-skills.md ~/src/openclaw/docs/tools/skills-config.md ~/src/openclaw/docs/tools/skills.md ~/src/openclaw/docs/zh-CN/cli/skills.md ~/src/openclaw/docs/zh-CN/platforms/mac/skills.md ~/src/openclaw/docs/zh-CN/tools/creating-skills.md ~/src/openclaw/docs/zh-CN/tools/skills-config.md ~/src/openclaw/docs/zh-CN/tools/skills.md ~/src/openclaw/extensions/feishu/skills/feishu-doc/SKILL.md ~/src/openclaw/extensions/feishu/skills/feishu-drive/SKILL.md ~/src/openclaw/extensions/feishu/skills/feishu-perm/SKILL.md ~/src/openclaw/extensions/feishu/skills/feishu-wiki/SKILL.md ~/src/openclaw/extensions/lobster/SKILL.md ~/src/openclaw/extensions/open-prose/skills/prose/SKILL.md ~/src/openclaw/skills/1password/SKILL.md ~/src/openclaw/skills/apple-notes/SKILL.md ~/src/openclaw/skills/apple-reminders/SKILL.md ~/src/openclaw/skills/bear-notes/SKILL.md ~/src/openclaw/skills/blogwatcher/SKILL.md ~/src/openclaw/skills/blucli/SKILL.md ~/src/openclaw/skills/bluebubbles/SKILL.md ~/src/openclaw/skills/camsnap/SKILL.md ~/src/openclaw/skills/canvas/SKILL.md ~/src/openclaw/skills/clawhub/SKILL.md ~/src/openclaw/skills/coding-agent/SKILL.md ~/src/openclaw/skills/discord/SKILL.md ~/src/openclaw/skills/eightctl/SKILL.md ~/src/openclaw/skills/food-order/SKILL.md ~/src/openclaw/skills/gemini/SKILL.md ~/src/openclaw/skills/gifgrep/SKILL.md ~/src/openclaw/skills/github/SKILL.md ~/src/openclaw/skills/gog/SKILL.md ~/src/openclaw/skills/goplaces/SKILL.md ~/src/openclaw/skills/healthcheck/SKILL.md ~/src/openclaw/skills/himalaya/SKILL.md ~/src/openclaw/skills/imsg/SKILL.md ~/src/openclaw/skills/local-places/SKILL.md ~/src/openclaw/skills/mcporter/SKILL.md ~/src/openclaw/skills/model-usage/SKILL.md ~/src/openclaw/skills/nano-banana-pro/SKILL.md ~/src/openclaw/skills/nano-pdf/SKILL.md ~/src/openclaw/skills/notion/SKILL.md ~/src/openclaw/skills/obsidian/SKILL.md ~/src/openclaw/skills/openai-image-gen/SKILL.md ~/src/openclaw/skills/openai-whisper-api/SKILL.md ~/src/openclaw/skills/openai-whisper/SKILL.md ~/src/openclaw/skills/openhue/SKILL.md ~/src/openclaw/skills/oracle/SKILL.md ~/src/openclaw/skills/ordercli/SKILL.md ~/src/openclaw/skills/peekaboo/SKILL.md ~/src/openclaw/skills/sag/SKILL.md ~/src/openclaw/skills/session-logs/SKILL.md ~/src/openclaw/skills/sherpa-onnx-tts/SKILL.md ~/src/openclaw/skills/skill-creator/SKILL.md ~/src/openclaw/skills/slack/SKILL.md ~/src/openclaw/skills/songsee/SKILL.md ~/src/openclaw/skills/sonoscli/SKILL.md ~/src/openclaw/skills/spotify-player/SKILL.md ~/src/openclaw/skills/summarize/SKILL.md ~/src/openclaw/skills/things-mac/SKILL.md ~/src/openclaw/skills/tmux/SKILL.md ~/src/openclaw/skills/trello/SKILL.md ~/src/openclaw/skills/video-frames/SKILL.md ~/src/openclaw/skills/voice-call/SKILL.md ~/src/openclaw/skills/wacli/SKILL.md ~/src/openclaw/skills/weather/SKILL.md
Particularly useful when the model needs to find relevant implementation files, configurations, or related code.
Note that this system is NOT limited to a single project folder.
Context Retrieval tooling
A general decision I made early on for the following tooling was to always include line numbers in front of lines, eg:
L1: function add(a, b) {
L2: return a + b;
L3: }
... and so on
This makes references unambiguous for both model and user, and it composes cleanly with edit and future retrieval operations.
find_matching_lines
Searches for regex patterns across files, directories, or buffers using ripgrep. Returns matching lines and line numbers, with optional context lines before and after the match.
Source
Helper Function
(defun my/content-utils-search-ripgrep (source pattern &optional ctx)
"Search SOURCE (file or directory) with ripgrep for PATTERN.
Expands ~ and environment variables in SOURCE.
Returns rg output on match, or a no-match message."
(unless (executable-find "rg")
(error "ripgrep (rg) not found in PATH"))
(let* ((expanded (expand-file-name
(substitute-in-file-name source))))
(unless (file-exists-p expanded)
(error "Path does not exist: %s" expanded))
(unless (or (file-regular-p expanded)
(file-directory-p expanded))
(error "Not a file or directory: %s" expanded))
(when (and ctx (not (and (integerp ctx) (>= ctx 0))))
(error "ctx must be a non-negative integer or nil, got: %S" ctx))
(let* ((ctx-args (when (and (integerp ctx) (> ctx 0))
(list "--context" (number-to-string ctx))))
(args (append
'("--line-number"
"--no-heading"
"--with-filename")
ctx-args
(list "--" pattern expanded))))
(with-temp-buffer
(let ((exit-code (apply #'call-process "rg" nil t nil args)))
(cond
((zerop exit-code)
(string-trim (buffer-string)))
((= exit-code 1)
(format "No matches found for pattern '%s' under %s"
pattern expanded))
(t
(error "ripgrep failed (exit %d) for pattern '%s' under %s"
exit-code pattern expanded))))))))
Tool Definition
(gptel-make-tool
:name "find_matching_lines"
:function #'my/content-utils-search-ripgrep
:description "Search a file or directory using ripgrep (rg). Expands ~ and environment variables in the path. Returns matching lines with filenames and line numbers; optional context supported."
:args (list
'(:name "source"
:type string
:description "Path to a file or directory to search. ~, relative paths, and $VARS are allowed.")
'(:name "pattern"
:type string
:description "Ripgrep regex pattern to search for. (Passed after -- to avoid option parsing.)")
'(:name "ctx"
:type number
:description "Optional non-negative integer context lines before/after each match. Use 0 or omit for none."))
:category "discovery")
Example Interaction
Tool Callsearch_path_rgsource ~/src/openclaw/skills pattern ```bash|```sh ctx 5
▸
| source | ~/src/openclaw/skills |
| pattern | ```bash|```sh |
| ctx | 5 |
~/src/openclaw/skills/voice-call/SKILL.md-16- ~/src/openclaw/skills/voice-call/SKILL.md-17-Use the voice-call plugin to start or inspect calls (Twilio, Telnyx, Plivo, or mock). ~/src/openclaw/skills/voice-call/SKILL.md-18- ~/src/openclaw/skills/voice-call/SKILL.md-19-## CLI ~/src/openclaw/skills/voice-call/SKILL.md-20- ~/src/openclaw/skills/voice-call/SKILL.md:21:```bash ~/src/openclaw/skills/voice-call/SKILL.md-22-openclaw voicecall call --to "+15555550123" --message "Hello from OpenClaw" ~/src/openclaw/skills/voice-call/SKILL.md-23-openclaw voicecall status --call-id <id> ~/src/openclaw/skills/voice-call/SKILL.md-24-``` ~/src/openclaw/skills/voice-call/SKILL.md-25- ~/src/openclaw/skills/voice-call/SKILL.md-26-## Tool -- ~/src/openclaw/skills/imsg/SKILL.md-39- ~/src/openclaw/skills/imsg/SKILL.md-40-## Actions ~/src/openclaw/skills/imsg/SKILL.md-41- ~/src/openclaw/skills/imsg/SKILL.md-42-### List chats ~/src/openclaw/skills/imsg/SKILL.md-43- ~/src/openclaw/skills/imsg/SKILL.md:44:```bash ~/src/openclaw/skills/imsg/SKILL.md-45-imsg chats --limit 10 --json ~/src/openclaw/skills/imsg/SKILL.md-46-``` ~/src/openclaw/skills/imsg/SKILL.md-47- ~/src/openclaw/skills/imsg/SKILL.md-48-### Fetch chat history ~/src/openclaw/skills/imsg/SKILL.md-49- ~/src/openclaw/skills/imsg/SKILL.md:50:```bash ~/src/openclaw/skills/imsg/SKILL.md-51-imsg history --chat-id 1 --limit 20 --attachments --json ~/src/openclaw/skills/imsg/SKILL.md-52-``` ~/src/openclaw/skills/imsg/SKILL.md-53- ~/src/openclaw/skills/imsg/SKILL.md-54-### Watch a chat ~/src/openclaw/skills/imsg/SKILL.md-55- ~/src/openclaw/skills/imsg/SKILL.md:56:```bash ~/src/openclaw/skills/imsg/SKILL.md-57-imsg watch --chat-id 1 --attachments ~/src/openclaw/skills/imsg/SKILL.md-58-``` ~/src/openclaw/skills/imsg/SKILL.md-59- ~/src/openclaw/skills/imsg/SKILL.md-60-### Send a message ~/src/openclaw/skills/imsg/SKILL.md-61- ~/src/openclaw/skills/imsg/SKILL.md:62:```bash ~/src/openclaw/skills/imsg/SKILL.md-63-imsg send --to "+14155551212" --text "hi" --file /path/pic.jpg ~/src/openclaw/skills/imsg/SKILL.md-64-``` ~/src/openclaw/skills/imsg/SKILL.md-65- ~/src/openclaw/skills/imsg/SKILL.md-66-## Notes ~/src/openclaw/skills/imsg/SKILL.md-67- -- ~/src/openclaw/skills/notion/SKILL.md-17- ~/src/openclaw/skills/notion/SKILL.md-18-1. Create an integration at https://notion.so/my-integrations ~/src/openclaw/skills/notion/SKILL.md-19-2. Copy the API key (starts with `ntn_` or `secret_`) ~/src/openclaw/skills/notion/SKILL.md-20-3. Store it: ~/src/openclaw/skills/notion/SKILL.md-21- ~/src/openclaw/skills/notion/SKILL.md:22:```bash ~/src/openclaw/skills/notion/SKILL.md-23-mkdir -p ~/.config/notion ------ note: manually trimmed output here ✂︎ ------
For some reason the current breed of LLM models are extremely good at leveraging this tool for:
- finding function definitions
- finding usage patterns and coding styles
- understanding how features are implemented across the codebase
In fact, at one point I enthusiastically added an LSP aware tool, but ended up removing it since many models prefer to use simple grep-like exploration.
get_lines_range
Returns a contiguous line interval from a file or buffer.
Compared to ad hoc whole-file reads, gives controlled chunking with explicit boundaries.
Used in conjunction with find matching lines above, the models can explore a codebase quite effectively.
Source
Helper Function
(defun my/gptel-content-read (source &optional start-line end-line)
"Read SOURCE (buffer or file) with L<n>: prefixes.
START-LINE and END-LINE are 1-based line numbers."
;; Enhanced input validation
(when (and start-line (not (integerp start-line)))
(error "start-line must be an integer, got: %s" (type-of start-line)))
(when (and end-line (not (integerp end-line)))
(error "end-line must be an integer, got: %s" (type-of end-line)))
(when (and start-line end-line (> start-line end-line))
(error "start-line (%d) cannot be greater than end-line (%d)" start-line end-line))
(pcase source
((pred stringp)
(cond
((get-buffer source)
;; It's a buffer name
(let* ((buffer (get-buffer source))
(body (gptel-content-utils--buffer-lines buffer start-line end-line))
(out (gptel-content-utils--format-lines body (or start-line 1))))
(format "Buffer: %s\n%s" source (string-join out "\n"))))
((file-readable-p (expand-file-name source))
;; It's a file path - use temporary buffer for better memory management
(with-temp-buffer
(insert-file-contents (expand-file-name source))
(gptel-content-read (current-buffer) start-line end-line)))
(t (error "Source =%s= is not a buffer or readable file" source))))
((pred bufferp)
;; It's a buffer object - ensure it's live
(unless (buffer-live-p source)
(error "Buffer is not live: %s" source))
(let* ((body (gptel-content-utils--buffer-lines source start-line end-line))
(out (gptel-content-utils--format-lines body (or start-line 1))))
(format "Buffer: %s\n%s" (buffer-name source) (string-join out "\n"))))
(_ (error "Source =%s= must be a string (buffer name/file path) or buffer object" source))))
Tool Definition
(gptel-make-tool
:name "get_lines_range"
:function #'my/gptel-content-read
:description "Return contiguous line range with line numbers."
:args (list '(:name "source" :type string :description "Path to a file or buffer name.")
'(:name "start_line" :type integer :description "Starting line (1-based).")
'(:name "end_line" :type integer :description "Ending line (1-based)."))
:category "retrieval")
Example Interaction
Tool Callfind_matching_linessource ~/.doom.d/local/config-files/macos.el pattern color context_lines 5
▸
| source | ~/.doom.d/local/config-files/macos.el |
| pattern | color |
| context_lines | 5 |
4- 5- ;; Japanese input support 6- (define-key global-map [?¥] [?\\])) 7- 8-(when is-mac 9: (defun nscolor2hex (color) 10: "Converts colors from `NSColor' format to hex values" 11- (concat "#" 12- (mapconcat (lambda (x) 13- (format "%x" (lsh (string-to-number x) -8))) 14: (split-string (s-replace "\"" "" color) ",") 15- ""))) 16- 17: (defun color-picker (&optional list buffer-name callback) 18: "Calls OS X color picker and insert the chosen color" 19- (interactive) 20- (let ((result (do-applescript "tell application \"Finder\" 21- activate 22- set result to \"\" 23: set x to (choose color) 24- set result to item 1 of x as string 25- set result to result & \",\" 26- set result to result & item 2 of x as string 27- set result to result & \",\" 28- set result to result & item 3 of x as string 29- return result 30- end tell"))) 31- (if callback 32: (funcall callback (nscolor2hex result)) 33: (insert (nscolor2hex result))) 34- (do-applescript "tell application \"Emacs\" to activate"))) 35- 36- (map! :leader 37: :desc "Color picker" :nv "o c" #'color-picker)) 38- 39-(when is-mac 40- (defun my/refresh-chrome () 41- "Refresh active Chrome tab" 42- (interactive)
Tool Callget_lines_rangesource ~/.doom.d/local/config-files/macos.el start_line 9 end_line 37
▸
| source | ~/.doom.d/local/config-files/macos.el |
| start_line | 9 |
| end_line | 37 |
Buffer: *temp* L9: (defun nscolor2hex (color) L10: "Converts colors from `NSColor' format to hex values" L11: (concat "#" L12: (mapconcat (lambda (x) L13: (format "%x" (lsh (string-to-number x) -8))) L14: (split-string (s-replace "\"" "" color) ",") L15: ""))) L16: L17: (defun color-picker (&optional list buffer-name callback) L18: "Calls OS X color picker and insert the chosen color" L19: (interactive) L20: (let ((result (do-applescript "tell application \"Finder\" L21: activate L22: set result to \"\" L23: set x to (choose color) L24: set result to item 1 of x as string L25: set result to result & \",\" L26: set result to result & item 2 of x as string L27: set result to result & \",\" L28: set result to result & item 3 of x as string L29: return result L30: end tell"))) L31: (if callback L32: (funcall callback (nscolor2hex result)) L33: (insert (nscolor2hex result))) L34: (do-applescript "tell application \"Emacs\" to activate"))) L35: L36: (map! :leader L37: :desc "Color picker" :nv "o c" #'color-picker))
The models tend to be able to use this for:
- Pulling function bodies and nearby logic around a known anchor.
- Iterative expansion (small window first, widen only when needed).
- Keeping context dense and relevant during implementation or debugging.
read_content
Reads a full file or buffer into the prompt context.
Tool Definition
(gptel-make-tool
:name "read_content"
:function #'my/gptel-content-read
:description "Read entire buffer/file with L<n>: prefixes. PREFER get_lines_by_numbers for specific lines."
:args (list '(:name "source" :type string :description "Path to a file or name of an existing buffer.")
:category "retrieval")
A blunt hammer, but sometimes useful, but is discouraged in the tool description since it can easily blow out the token count.
Research tooling
kagi_search
Searches using Kagi's API for high-quality, ad-free results. Supports search operators, exact phrases with single quotes, and Boolean operators (AND, OR, +, -).
Source
(list :name "kagi_search"
:function (lambda (callback query)
(gptel-web-kagi-search
callback
(if (string-match-p "'" query)
(replace-regexp-in-string "'" "\\\\'" query)
query)))
:description "Search for information using Kagi.
Search Operators
filetype: — Returns only search results that match a particular file extension, for example: us census 1860 filetype:pdf. The filetypes available in Kagi Search come from anonymized queries of the available Google filetypes.
site: — Returns only search results from a particular website, for example: best in show dog site:akc.org
inurl: — Finds webpages whose URL includes a term of phrase, for example: best headphones inurl:forum
intitle: — Finds webpages whose title includes a term of phrase, for example: chess intitle:books
'word' — Finds the exact words in a phrase, for example: 'survival is insufficient'
() — Finds or excludes webpages that contain a group of words, examples below
AND — Finds webpages that contain all the terms or phrases, for example: sweaters (christmas AND ugly)
+ and - — Finds webpages that contain or exclude a term or phrase, for example: food +cat -dog
OR — Finds webpages that contain either of the terms or phrases, for example: recipes (szechuan OR cantonese)
* — Matches any word, for example: best * ever
"
:args (list '(:name "query" :type string :description "The search query"))
:category "web"
:async t
:include t)
Example Interaction
Tool Callkagi_searchquery emacs coffee -coffeescript site:github.com
▸
| query | emacs coffee -coffeescript site:github.com |
There are several GitHub repositories related to "emacs coffee" and "coffeescript." ,**Emacs Themes:** * **Coffee Theme:** A warm theme for the GNU Emacs editor is available at `coffee-theme/coffee.emacs` 【5】【8】. This repository has been migrated to a self-hosted instance 【5】. ,**Emacs Packages and Configurations:** * **`coffee.el`:** This file is part of `emacs-goodies-el` and contains code related to coffee 【6】. * **`company-coffee`:** An Emacs add-on that incorporates coffee keywords into `company-keywords` for completion purposes 【2】. * **`A-cup-of-coffee-with-Emacs`:** A GitHub repository for contributing to development related to Emacs and coffee 【3】. * **`.emacs.d-1`:** A repository containing Emacs configurations, with a coffee emoji in its name 【7】. ,**Games and Projects:** * **Roguelike Game:** A "coffee-break roguelike" for Emacs is being developed, aiming to allow players to create characters and engage in challenges like arena fights and dungeon explorations 【1】. ,**Other:** * **Emacs Mirroring:** A repository exists for mirroring GNU Emacs and ELPA repositories from Savannah to GitHub 【4】. [1] [[https://github.com/stevenremot/roguel-ike][GitHub - stevenremot/roguel-ike: Coffee-break roguelike for Emacs]]: The goal of this project is to develop a coffee-break roguelike for Emacs. The complete game will allow the player to create various characters and to make them fulfill challenges like arena fight and short dungeon explorations. There isn't any level system. [2] [[https://github.com/nverno/company-coffee][GitHub - nverno/company-coffee: Emacs add coffee keywords to...]]: nverno/company-coffee. This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.View code. About. Emacs add coffee keywords to company-keywords for completion. Activity. [3] [[https://github.com/Gelius2881/A-cup-of-coffee-with-Emacs][Gelius2881/A-cup-of-coffee-with-Emacs - GitHub]]: Contribute to Gelius2881/A-cup-of-coffee-with-Emacs development by creating an account on GitHub. [4] [[https://github.com/emacsmirror/.savannah][emacsmirror/.savannah: Mirrors of GNU Emacs, GNU ELPA ... - GitHub]]: Mirrors of GNU Emacs, GNU ELPA and NonGNU ELPA. This repository contains the tooling used to mirror these three repositories from Savannah to GitHub. The mirror ... [5] [[https://github.com/coffee-theme/coffee.emacs][coffee-theme/coffee.emacs: [Migrated to self-hosted ari-web ... - GitHub]]: This repository has been migrated to the self-hosted ari-web Forgejo instance: https://git.ari.lt/coffee/coffee.emacs.[Migrated to self-hosted ari-web Forgejo: https://git.ari.lt/coffee/coffee.emacs] A warm theme for GNU emacs. [6] [[https://github.com/deestan/emacs/blob/master/emacs-goodies-el/coffee.el][emacs/emacs-goodies-el/coffee.el at master - GitHub]]: While the code is focused, press Alt+F1 for a menu of operations. [7] [[https://github.com/yamad/.emacs.d-1][GitHub - yamad/.emacs.d-1: :coffee: My Emacs configurations; but...]]: Build Emacs 24.5 from source on Travis.Projectile commands Project File Flycheck (flycheck-compile) for debugging ansi-term Quoting characters C-q Partially evaluate list elements Temporarily disable ido-completion C-j Set directory local variable eval to execute arbitrary code View Lossage Smartparens Compiling Emacs from source update prepare Arch CentOS 7 Ubuntu 14.04 Windows Follow nt/INSTALL Fix line endings autoreconf configure Arch OS X CentOS 7. [8] [[https://github.com/topics/coffee-theme?l=emacs+lisp&o=desc&s=stars][coffee-theme · GitHub Topics · GitHub]]: coffee-theme / coffee.emacs Star 0 Code Issues Pull requests A warm theme for GNU emacs editor linux theme lisp emacs emacs-lisp coffee gnu dark warm coffee-theme Updated Nov 11, 2022 Emacs Lisp
Tool Callkagi_searchquery 'coffee.el' emacs-goodies-el what does it do
▸
| query | 'coffee.el' emacs-goodies-el what does it do |
The `coffee.el` Emacs module, part of `emacs-goodies-el`, provides an interface for Emacs users to interact with **RFC2324-compliant coffee devices** 【1】【2】. This protocol is known as the **Hyper Text Coffee Pot Control Protocol (HTCPCP)** 【1】【2】. Essentially, `coffee.el` allows you to: - **Submit a "BREW" request** to a coffee device 【2】【4】. - **Prompt the user for different additives** to the coffee 【1】. - **Communicate with a coffee device** that implements the HTCPCP 【1】【2】. Some versions of `coffee.el` may require a special BREW-capable version of Emacs/W3 【3】. [1] [[https://github.com/helgeholm/emacs/blob/master/emacs-goodies-el/coffee.el][emacs/emacs-goodies-el/coffee.el at master · helgeholm/emacs]]: Contribute to helgeholm/emacs development by creating an account on GitHub.;; This module provides an Emacs interface to RFC2324-compliant coffee. ;; devices (Hyper Text Coffee Pot Control Protocol, or HTCPCP). It. ;; prompts the user for the different additives, then issues a BREW. ;; request to the coffee device. [2] [[https://github.com/emarsden/coffee-el][GitHub - emarsden/coffee-el: Submit a BREW request to an RFC2324 ...]]: This Emacs Lisp library allows Emacs users to submit a BREW request to an RFC2324-compliant coffee device (implementing the Hyper Text Coffee Pot Control Protocol, or HTCPCP). [3] [[https://github.com/deestan/emacs/blob/master/emacs-goodies-el/coffee.el][emacs/emacs-goodies-el/coffee.el at master]]: ;; request to the coffee device. ;;. ;; coffee.el requires a special BREW-capable version of Emacs/W3 to be. [4] [[https://github.com/emarsden/coffee-el/issues][Issues]]: Submit a BREW request to an RFC2324-compliant coffee device from Emacs. - Issues · emarsden/coffee-el
Great for surfacing relevant libraries or getting a summary of sentiment about a topic.
read_url
A tool similar to curl but with a few extra caveats.
- Always uses a Chrome-like header to avoid being blocked by servers that discriminate against automated requests.
- Inspects the
Content-Typeresponse header and handles different formats appropriately:- HTML: Uses Emacs's
libxmlandshrto parse the DOM and render the page as readable text, stripping away scripts, styles, and navigation noise - Plain text: Returns the content as-is
- Fallback: Attempts to return whatever content was retrieved
- HTML: Uses Emacs's
Source
Helper Function
(defun my/web-read-url (url)
"Fetch and read the textual content of a URL using Chrome-like headers.
Parses HTML into readable text."
(let ((url-request-extra-headers
'(("User-Agent" . "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36")
("Accept" . "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
("Accept-Language" . "en-US,en;q=0.9")
("Accept-Encoding" . "gzip, deflate, br")
("DNT" . "1")
("Connection" . "keep-alive")
("Upgrade-Insecure-Requests" . "1")
("Sec-Fetch-Dest" . "document")
("Sec-Fetch-Mode" . "navigate")
("Sec-Fetch-Site" . "none")
("Sec-Fetch-User" . "?1")
("Cache-Control" . "max-age=0"))))
(let ((url-buffer (url-retrieve-synchronously url t t 10))
result)
(unless url-buffer
(error "Failed to retrieve URL: %s" url))
(with-current-buffer url-buffer
(goto-char (point-min))
(re-search-forward "^$" nil 'move)
(forward-char)
(if (fboundp 'libxml-parse-html-region)
(let ((dom (libxml-parse-html-region (point) (point-max))))
(setq result
(with-temp-buffer
(shr-insert-document dom)
(buffer-string))))
;; Fallback: return raw content
(setq result (buffer-substring-no-properties (point) (point-max))))
(kill-buffer url-buffer))
(if result
(string-trim result)
(error "Failed to retrieve content from URL: %s" url)))))
Tool Definition
(gptel-make-tool
:name "read_url"
:function #'my/web-read-url
:description "Fetch and read the textual content of a URL"
:args (list
'(:name "url" :type string :description "The URL to read."))
:category "web")
Example Interaction
Tool Callread_urlurl https://github.com/emarsden/coffee-el
▸
| url | https://github.com/emarsden/coffee-el |
GitHub - emarsden/coffee-el: Submit a BREW request to an RFC2324-compliant
coffee device from Emacs.
Skip to content
Navigation Menu
Toggle navigation
SVG Image
Sign in
SVG ImageNotifications You must be signed in to change notification settings
*
SVG ImageFork 0
*
SVG Image Star 1
Submit a BREW request to an RFC2324-compliant coffee device from Emacs.
License
SVG Image GPL-2.0 license
SVG Image1 star
SVG Image0 forks
SVG ImageBranches
SVG ImageTags
SVG ImageActivity
SVG Image Star
SVG ImageNotifications You must be signed in to change notification settings
*
SVG ImageCode
*
SVG ImageIssues 0
*
SVG ImagePull requests 0
*
SVG ImageActions
*
SVG ImageProjects 0
*
SVG ImageSecurity 0
*
SVG ImageInsights
SVG ImageAdditional navigation options
*
SVG Image Code
*
SVG Image Issues
*
SVG Image Pull requests
*
SVG Image Actions
*
SVG Image Projects
*
SVG Image Security
*
SVG Image Insights
emarsden/coffee-el
SVG Image
main
SVG Image
SVG ImageBranches
SVG ImageTags
SVG Image
SVG Image
Go to file
SVG ImageCode
SVG Image
SVG ImageOpen more actions menu
Folders and files
Name Name Last commit message
Latest commit
History
3 Commits
3 Commits
.gitignore .gitignore
LICENSE LICENSE
README.md README.md
coffee.el coffee.el
View all files
Repository files navigation
*
SVG ImageREADME
*
SVG ImageGPL-2.0 license
SVG Image
coffee.el -- Submit a BREW request to an RFC2324-compliant coffee device
SVG Image
License: GPL v2
This Emacs Lisp library allows Emacs users to submit a BREW request to an
RFC2324-compliant coffee device (implementing the Hyper Text Coffee Pot Control
Protocol, or HTCPCP). It prompts the user for the different additives, then issues a
HTCPCP BREW request to the coffee device.
You can probably guess how coffee.el came about …yadda yadda everything but
make coffee …yadda yadda…
coffee.el requires a special BREW-capable version of Emacs/W3 to be installed.
References
SVG Image
See RFC 2324.
See also coffee-mode, a major mode for CoffeeScript and IcedCoffeeScript. There is
also a coffee.el meetup organized in San Francisco.
The latest version of this package should be available from
https://github.com/emarsden/coffee-el/
About
Submit a BREW request to an RFC2324-compliant coffee device from Emacs.
Topics
lisp emacs coffee-machine
Resources
SVG Image Readme
License
SVG Image GPL-2.0 license
Footer
SVG Image © 2026 GitHub, Inc.
Footer navigation
* Terms
* Privacy
* Security
* Status
* Community
* Docs
* Contact
* Manage cookies
* Do not share my personal information
SVG Image
SVG Image You can’t perform that action at this time.The key benefit is getting clean, readable content rather than raw markup. Raw HTML is noisy and has a lot of irrelevant info in there.
extract_page_links
This tool extracts all links from a web page, returning either just the URLs or URL + link text pairs. The links can be optionally filtered using a regex.
Source
Helper Function
(defun gptel-web-extract-links (url &optional include-text filter-pattern)
"Extract all links from a web page at URL.
If INCLUDE-TEXT is non-nil, return (url . text) pairs.
If FILTER-PATTERN is provided, filter links by URL pattern."
(let ((links '())
(url-request-extra-headers
'(("User-Agent" . "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"))))
(with-current-buffer (url-retrieve-synchronously url t)
(unwind-protect
(progn
(goto-char (point-min))
(when (re-search-forward "^$" nil t)
(forward-line 1))
(let ((dom (libxml-parse-html-region (point) (point-max))))
(when dom
(dolist (link-element (dom-by-tag dom 'a))
(let ((href (dom-attr link-element 'href))
(text (string-trim (dom-texts link-element))))
(when href
(setq href (url-expand-file-name href url))
(push (cons href text) links)))))))
(kill-buffer (current-buffer))))
(setq links (delete-dups (nreverse links)))
(when filter-pattern
(setq links (seq-filter (lambda (link)
(string-match-p filter-pattern (car link)))
links)))
(if include-text
links
(mapcar #'car links))))
Tool Definition
(gptel-make-tool
:name "extract_page_links"
:function #'gptel-web-extract-links
:description "Extract all links from a web page. Returns URLs and optionally link text."
:args (list
'(:name "url" :type string :description "The URL of the web page to extract links from.")
'(:name "include-text" :type boolean :description "Whether to include link text." :optional t)
'(:name "filter-pattern" :type string :description "Optional regex pattern to filter links." :optional t))
:category "web")
Example Interaction
Tool Callkagi_searchquery openclaw web page
▸
| query | openclaw web page |
,**OpenClaw** is an open-source personal AI assistant and autonomous agent that runs on your local machine. It is designed to act as a teammate, capable of performing various tasks such as managing your inbox, sending emails, scheduling appointments, and checking in for flights. OpenClaw integrates with chat applications like WhatsApp, Telegram, Discord, and Slack, allowing you to interact with it through familiar interfaces 【1】【3】.
Key features and capabilities of OpenClaw include:
* **Autonomous Operation**: OpenClaw can browse the web, read and write files, and execute shell commands independently 【2】.
* **Local Data Storage**: Your data and skills are stored locally, preventing lock-in with large tech companies 【2】.
* **Extensibility**: It supports various communication channels and can be extended with new skills 【7】.
* **Web Tools**: OpenClaw includes web tools for searching the internet (via Brave Search or Perplexity Sonar) and fetching web content 【4】.
* **Security**: The platform incorporates security measures, such as treating browser/web content as untrusted by default 【5】.
* **Installation**: It can be installed through various methods, including an Ansible playbook for automated setup and a Flutter app for Android 【8】【9】.
For getting started, you can configure OpenClaw, including setting up web search API keys, and check its health status 【6】. The platform also exposes webhook endpoints for web-facing interactions 【10】.
[1] [[https://openclaw.ai/][OpenClaw — Personal AI Assistant]]: OpenClaw The AI that actually does things. Clears your inbox, sends emails, manages your calendar, checks you in for flights. All from WhatsApp, Telegram, or any chat app you already use. New OpenClaw Partners with VirusTotal for Skill Security →
[2] [[https://open-claw.org/][OpenClaw | The Open-Source Personal AI Assistant & Autonomous Agent]]: OpenClaw is a 24/7 open-source personal AI assistant that lives on your own machine. Unlike standard chatbots like ChatGPT, it has 'eyes and hands'—it can browse the web, read and write files, and run shell commands autonomously. It is designed to be a teammate, not just a tool, capable of handling everything from taxes to code reviews. Its context and skills reside locally, meaning you are not locked into a tech giant's ecosystem.
[3] [[https://openclaw.ai/blog/introducing-openclaw][Introducing OpenClaw]]: OpenClaw is an open agent platform that runs on your machine and works from the chat apps you already use. WhatsApp, Telegram, Discord, Slack, ...
[4] [[https://docs.openclaw.ai/tools/web][Web Tools - OpenClaw]]: OpenClaw ships two lightweight web tools: web_search — Search the web via Brave Search API (default) or Perplexity Sonar (direct or via OpenRouter). web_fetch — HTTP fetch + readable extraction (HTML → markdown/text).Run openclaw configure --section web to store the key in config (recommended), or set BRAVE_API_KEY in your environment.
[5] [[https://github.com/openclaw/openclaw/releases][Releases · openclaw/openclaw - GitHub]]: Thanks @1seal. Security/Web tools: treat browser/web content as untrusted by default (wrapped outputs for browser snapshot/tabs/console and structured external-content metadata for web tools), and strip toolResult.details from model-facing transcript/compaction inputs to reduce prompt-injection replay risk.
[6] [[https://docs.openclaw.ai/start/getting-started][Getting Started - OpenClaw]]: ... Easiest path: openclaw configure --section web (stores tools.web.search.apiKey ). ... If openclaw health shows “no auth configured”, go back to the wizard and set OAuth/key auth — the agent won’t be able to respond without it. ... Health probes: openclaw health (or openclaw status --deep ) asks the running gateway for a health snapshot.
[7] [[https://github.com/openclaw/openclaw][openclaw/openclaw: Your own personal AI assistant. Any ...]]: OpenClaw is a personal AI assistant you run on your own devices. It answers you on the channels you already use (WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, Microsoft Teams, WebChat), plus extension channels like BlueBubbles, Matrix, Zalo, and Zalo Personal.
[8] [[https://github.com/openclaw][openclaw - GitHub]]: Your personal, open source AI assistant. openclaw has 18 repositories available. Follow their code on GitHub.openclaw-ansible openclaw-ansible Public. Automated, hardened Clawdbot installation with Tailscale VPN, UFW firewall, and Docker isolation.
[9] [[https://github.com/mithun50/openclaw-termux][GitHub - mithun50/openclaw-termux: Run OpenClaw AI Gateway on Android ...]]: Run OpenClaw AI Gateway on Android — standalone Flutter app with built-in terminal, web dashboard, optional dev tools, and one-tap setup. Also available as a Termux CLI package.
[10] [[https://docs.openclaw.ai/web][Web - OpenClaw]]: ... This page focuses on bind modes, security, and web-facing surfaces. ... When hooks.enabled=true , the Gateway also exposes a small webhook endpoint on the same HTTP server. ... gateway : { bind : " loopback " , tailscale : { mode : " funnel " }, auth : { mode : " password " }, // or OPENCLAW_GATEWAY_PASSWORD }, }Tool Callextract_page_linksurl https://openclaw.ai/
▸
| url | https://openclaw.ai/ |
(("https://openclaw.ai/blog/virustotal-partnership" . "New OpenClaw Partners with VirusTotal for Skill Security →") ("https://openclaw.ai/shoutouts" . "View all community shoutouts →") ("https://x.com/jonahships_/status/2010605025844723765" . "\"Setup @openclaw by @steipete yesterday. All I have to say is, wow. First I was using my Claude Max sub and I used all of my limit quickly, so today I had my claw bot setup a proxy to route my CoPilot subscription as a API endpoint so now it runs on that. It's the fact that claw can just keep building upon itself just by talking to it in discord is crazy. The future is already here\" @jonahships_") ("https://x.com/AryehDubois/status/2011742378655432791" . "\"Tried Claw by @steipete. I tried to build my own AI assistant bots before, and I am very impressed how many hard things Claw gets right. Persistent memory, persona onboarding, comms integration, heartbeats. A few minor wrinkles remain, but the end result is AWESOME.\" @AryehDubois") ("https://x.com/markjaquith/status/2010430366944055433" . "\"I've been saying for like six months that even if LLMs suddenly stopped improving, we could spend *years* discovering new transformative uses. @openclaw feels like that kind of \"just had to glue all the parts together\" leap forward. Incredible experience.\" @markjaquith") ("https://x.com/danpeguine/status/2014760164113477700" . "\"Why @openclaw is nuts: your context and skills live on YOUR computer, not a walled garden. It's open source. Growing community building skills. Only 19 days old and constantly improving. 'Personal AI assistant' undersells it — it's a company assistant, family assistant, team tool. Proactive AF: cron jobs, reminders, background tasks. Memory is amazing, context persists 24/7.\" @danpeguine") boss. Smooth as single malt.\" @Abhay08")
------
Note: Manually trimmed output here ✂︎
------
etc etc..It's useful to the model for:
- Site navigation: understanding a website's structure and discovering relevant pages
- Link discovery: finding documentation, references, or related content
- Pattern-based filtering: extracting only links matching a specific regex (e.g., documentation URLs, API endpoints, docs urls etc)
Action tooling
Most of these, apart from patch, are straightforward enough.
The patching system took some iteration and thinking to become fast enough to use, friendly enough for the model to generate but still "hands-on" enough to match the model of interaction I was aiming for.
There is no yolo-mode in this system unless explicitly prompted (eg. delete and re-make the files..). I mainly use this system for slow, observed changes - as a separate mode of interaction to one of the CLI coding agent tools.
create_file
Creates a new file at a given path and writes the provided content to it. Automatically creates missing parent directories, and is prevented from overwriting existing files
Source
Helper Function
(defun gptel-file-create (file-path content)
"Create a new file at FILE-PATH with CONTENT.
Returns success/error status."
(condition-case err
(let* ((full-path (expand-file-name file-path))
(dir (file-name-directory full-path)))
;; Ensure directory exists
(when dir
(make-directory dir t))
;; Write content to file
(with-temp-file full-path
(insert content))
(format "Created file: %s (%d chars)"
full-path
(length content)))
(error
(format "Error creating file %s: %s" file-path (error-message-string err)))))
Tool Definition
(gptel-make-tool
:name "create_file"
:function #'gptel-file-create
:description "Create a new file. Parent directories are created automatically if they don't exist."
:args (list '(:name "file_path" :type string :description "Path to the file to create (can use ~)")
'(:name "content" :type string :description "Content to write to the file"))
:category "files")
move_file
Moves or renames a file from a source path to a destination path.
Useful for refactors, reorganizing directories, or simple renames while preserving file contents.
Source
Helper Function
(defun gptel-file-move (source-path destination-path)
"Move or rename a file from SOURCE-PATH to DESTINATION-PATH.
Returns success/error status."
(condition-case err
(let ((src (expand-file-name source-path))
(dst (expand-file-name destination-path)))
(if (not (file-exists-p src))
(format "Error: Source file does not exist: %s" src)
(progn
;; Ensure destination directory exists
(make-directory (file-name-directory dst) t)
;; Move file (overwrite if exists)
(rename-file src dst t)
(format "Moved file: %s -> %s" src dst))))
(error
(format "Error moving file %s -> %s: %s"
source-path destination-path (error-message-string err)))))
Tool Definition
(gptel-make-tool
:name "move_file"
:function #'gptel-file-move
:description "Move or rename a file"
:args (list '(:name "source_path" :type string :description "Current path of the file (can use ~)")
'(:name "destination_path" :type string :description "New path for the file (can use ~)"))
:category "files")
delete_file
Deletes a file at the given path.
Source
Helper Function
(defun gptel-file-delete (file-path)
"Safely delete a file at FILE-PATH.
Returns success/error status."
(condition-case err
(let ((full-path (expand-file-name file-path)))
(if (not (file-exists-p full-path))
(format "Error: File does not exist: %s" full-path)
(progn
(delete-file full-path)
(format "Deleted file: %s" full-path))))
(error
(format "Error deleting file %s: %s" file-path (error-message-string err)))))
Tool Definition
(gptel-make-tool
:name "delete_file"
:function #'gptel-file-delete
:description "Delete a file"
:args (list '(:name "file_path" :type string :description "Path to the file to delete (can use ~)"))
:category "files")
Useful for clean-up after refactors, removing generated artifacts, or deleting obsolete files. Can also be used in conjunction with delete file as a sort of ad-hoc yolo mode.
patch
Creates a reviewable, editable diff block from old_text → new_text for a target file.
Patches appear as editable diff blocks in the buffer, to be applied with a shortcut. Application of a patch is then reflected under the patch block, so the model can pick up if it was accepted or rejected.
Tool Definition
(gptel-make-tool
:name "patch"
:function #'my/gptel-patch-create
:description "Create a patch by specifying old and new text. No line numbers needed - finds old text by searching (must be unique in file). Supports loose whitespace matching. The patch appears as an editable diff block that applies via search-replace."
:args (list
'(:name "name" :type string :description "Short patch name")
'(:name "description" :type string :description "What this patch changes")
'(:name "project_dir" :type string :description "Project root directory")
'(:name "file" :type string :description "File path relative to project_dir")
'(:name "old_text" :type string :description "Exact existing text to replace (must be unique)")
'(:name "new_text" :type string :description "Replacement text"))
:category "patch")
Example Interaction
Tool Callread_contentsource ~/weather.py
▸
| source | ~/weather.py |
Buffer: *temp*
L1: #!/usr/bin/env python3
L2: """
L3: Get today's weather for a list of cities.
L4:
L5: Usage:
L6: python weather_today.py London "New York" Tokyo
L7:
L8: Environment:
L9: OPENWEATHER_API_KEY=your_api_key_here
L10: """
L11:
L12: import os
L13: import sys
L14: import requests
L15:
L16: API_KEY = os.getenv("OPENWEATHER_API_KEY")
L17: BASE_URL = "https://api.openweathermap.org/data/2.5/weather"
L18:
L19:
L20: def get_weather(city: str, api_key: str) -> dict:
L21: params = {
L22: "q": city,
L23: "appid": api_key,
L24: "units": "metric", # Change to "imperial" for Fahrenheit
L25: }
L26: response = requests.get(BASE_URL, params=params, timeout=10)
L27: response.raise_for_status()
L28: data = response.json()
L29:
L30: return {
L31: "city": data.get("name", city),
L32: "country": data.get("sys", {}).get("country", ""),
L33: "temp_c": data.get("main", {}).get("temp"),
L34: "feels_like_c": data.get("main", {}).get("feels_like"),
L35: "humidity": data.get("main", {}).get("humidity"),
L36: "description": (data.get("weather") or [{}])[0].get("description", ""),
L37: "wind_mps": data.get("wind", {}).get("speed"),
L38: }
L39:
L40:
L41: def main():
L42: if not API_KEY:
L43: print("Error: OPENWEATHER_API_KEY is not set.")
L44: print("Set it with: export OPENWEATHER_API_KEY='your_api_key'")
L45: sys.exit(1)
L46:
L47: if len(sys.argv) < 2:
L48: print("Usage: python weather_today.py <city1> <city2> ...")
L49: sys.exit(1)
L50:
L51: cities = sys.argv[1:]
L52:
L53: for city in cities:
L54: try:
L55: w = get_weather(city, API_KEY)
L56: print(
L57: f"{w['city']}, {w['country']}: "
L58: f"{w['description'].title()}, "
L59: f"{w['temp_c']}°C (feels like {w['feels_like_c']}°C), "
L60: f"humidity {w['humidity']}%, wind {w['wind_mps']} m/s"
L61: )
L62: except requests.HTTPError as e:
L63: if e.response is not None and e.response.status_code == 404:
L64: print(f"{city}: city not found")
L65: else:
L66: print(f"{city}: HTTP error - {e}")
L67: except requests.RequestException as e:
L68: print(f"{city}: request failed - {e}")
L69: except Exception as e:
L70: print(f"{city}: unexpected error - {e}")
L71:
L72:
L73: if __name__ == "__main__":
L74: main()Tool Callpatchname feat: add better output formatting description Add formatted weather output with borders, columns, emoji icons, and color-coded temperature ranges for better readability project_dir ~ file weather.py
▸
| name | feat: add better output formatting |
| description | Add formatted weather output with borders, columns, emoji icons, and color-coded temperature ranges for better readability |
| project_dir | ~ |
| file | weather.py |
#+ATTR_PATCH: :name feat: add better output formatting #+ATTR_PATCH: :description Add formatted weather output with borders, columns, emoji icons, and color-coded temperature ranges for better readability #+ATTR_PATCH: :dir ~ #+ATTR_PATCH: :file weather.py #+ATTR_PATCH: :status pending
Tool Callpatchname feat: add formatted display function description Add function to display weather data with nice formatting including borders and columns project_dir ~ file weather.py
▸
| name | feat: add formatted display function |
| description | Add function to display weather data with nice formatting including borders and columns |
| project_dir | ~ |
| file | weather.py |
#+ATTR_PATCH: :name feat: add formatted display function #+ATTR_PATCH: :description Add function to display weather data with nice formatting including borders and columns #+ATTR_PATCH: :dir ~ #+ATTR_PATCH: :file weather.py #+ATTR_PATCH: :status pending
Tool Callpatchname feat: update main to use formatted display description Update main function to use the new formatted display function project_dir ~ file weather.py
▸
| name | feat: update main to use formatted display |
| description | Update main function to use the new formatted display function |
| project_dir | ~ |
| file | weather.py |
#+ATTR_PATCH: :name feat: update main to use formatted display #+ATTR_PATCH: :description Update main function to use the new formatted display function #+ATTR_PATCH: :dir ~ #+ATTR_PATCH: :file weather.py #+ATTR_PATCH: :status pending
You are free to accept all patches as they are, or review and guide the model. Applied patches / rejected patches and subsequent user added commentary also become very nice signal for the model.
This follows along nicely from how many models are trained to be used with TUI's such as claude code, so many of the modern "coding assistant" models handle this diff generation with flying colors.
Meta Tooling
If you have gone through and expanded any of the source blocks above, you will notice that the tools are defined in pairs of helper-function and then wrapper tool. This is becuase most of them (and other odder, less relevant tools not discussed here) were developed inside a gptel org-mode document using gptel itself.
Org-mode source blocks can be evaluated inline, which means the model can inspect existing elisp, propose changes, and iterate directly inside the document. It turns into an interesting and (dare I say it) fun form of agentic, REPL-driven co-development.
The result feels like a blend of:
- Literate programming: design, code and notes in one place
- Inline evaluation: fast feedback loop
- LLM-guided iteration: spec → test → refine
That pattern is powerful because it naturally encourages small, composable functions and makes the “runtime contract” explicit at the wrapper layer.
It’s a really strong workflow for building reliable, inspectable local agents in Emacs.
apropos
Searches Emacs symbols by name/pattern using the built-in apropos system, scoped by symbol type (functions, variables, commands, or all). Has some paging built in since it tends to return A LOT of results sometimes.
Source
Helper Function
(defun gptel-apropos-search (pattern &optional type limit offset)
"Search Emacs symbols using apropos with PATTERN.
TYPE can be one of: 'functions, 'variables, 'commands, or nil for all.
LIMIT controls max results (default 15). OFFSET skips initial results."
(condition-case err
(let* ((limit (or limit 15))
(offset (or offset 0))
(all-matches
(cl-remove-if-not
(lambda (sym)
(condition-case nil
(pcase type
((or 'nil "all" "")
t)
((or 'functions "functions")
(fboundp sym))
((or 'variables "variables")
(boundp sym))
((or 'commands "commands")
(commandp sym))
(_ t))
(error nil)))
(apropos-internal pattern)))
(total (length all-matches))
(paged (seq-take (nthcdr offset all-matches) limit)))
(if (null paged)
(format "No apropos matches for pattern: %s" pattern)
(concat
(format "Apropos matches (%d total, showing %d starting at %d):\n\n"
total (length paged) offset)
(mapconcat
(lambda (sym)
(let* ((is-fn (fboundp sym))
(is-var (boundp sym))
(is-cmd (commandp sym))
(kind (string-join
(delq nil
(list (when is-cmd "command")
(when (and is-fn (not is-cmd)) "function")
(when is-var "variable")))
", "))
(doc (cond
(is-var (documentation-property sym 'variable-documentation t))
(is-fn (documentation sym t))
(t nil)))
(doc1 (if (and doc (stringp doc))
(car (split-string (string-trim doc) "\n"))
"No doc available.")))
(format "- %s [%s]\n %s"
(symbol-name sym)
(if (string-empty-p kind) "symbol" kind)
doc1)))
paged
"\n\n"))))
(error
(format "Error running apropos for %s: %s"
pattern (error-message-string err)))))
Tool Definition
(gptel-make-tool
:name "apropos"
:function #'gptel-apropos-search
:description "Search for Emacs Lisp functions, variables, and commands matching a pattern using apropos."
:args (list
'(:name "pattern" :type string :description "Pattern to search for (regexp or substring).")
'(:name "type" :type string :description "Type of symbols to search: 'functions', 'variables', 'commands', or 'all'" :optional t)
'(:name "limit" :type integer :description "Maximum number of results to return (default 15)" :optional t)
'(:name "offset" :type integer :description "Number of results to skip for pagination (default 0)" :optional t))
:category "emacs")
Example Interaction
Tool Callapropospattern org.*src.*block type functions limit 20
▸
| pattern | org.*src.*block |
| type | functions |
| limit | 20 |
Found 39 functions matching 'org.*src.*block' (showing 1-20): • embrace--get-org-src-block-modes [function] • jupyter-org-insert-src-block [interactive] • jupyter-org-inspect-src-block [interactive] - Inspect the symbol under point when in a source block. • jupyter-org-move-src-block [interactive] • jupyter-org-next-busy-src-block [interactive] • jupyter-org-previous-busy-src-block [interactive] • jupyter-org-split-src-block [interactive] • ob-async-org-babel-execute-src-block [interactive] • org-babel-check-src-block [interactive] - Check for misspelled header arguments in the current code block. • org-babel-execute-src-block [interactive] • org-babel-execute-src-block-maybe [interactive] • org-babel-execute-src-block:async [interactive] • org-babel-exp-src-block [interactive] • org-babel-expand-src-block [interactive] • org-babel-expand-src-block-maybe [interactive] • org-babel-get-src-block-info [function] • org-babel-goto-named-src-block [interactive] - Go to a source-code block with NAME. (fn NAME) • org-babel-goto-src-block-head [interactive] - Go to the beginning of the current code block. • org-babel-map-inline-src-blocks [function] • org-babel-map-src-blocks [function] ... and 19 more. Use offset=20 to see next page.
describe_symbol
Retrieves the definition for an emacs-lisp symbol. Used in conjunction with the tool above to get more details.
Source
Helper Function
(defun gptel-elisp-describe-symbol (symbol-name &optional include-value)
"Describe Emacs symbol SYMBOL-NAME and return docs as text.
If INCLUDE-VALUE is non-nil and symbol is bound, include current value."
(condition-case err
(let* ((sym (intern-soft symbol-name)))
(if (null sym)
(format "No such symbol: %s" symbol-name)
(let* ((is-fn (fboundp sym))
(is-var (boundp sym))
(is-face (facep sym))
(kind (string-join
(delq nil
(list (when is-fn "function")
(when is-var "variable")
(when is-face "face")))
", "))
(fdoc (when is-fn (documentation sym t)))
(vdoc (when is-var (documentation-property sym 'variable-documentation t)))
(value-str
(when (and include-value is-var)
(condition-case nil
(format "%S" (symbol-value sym))
(error "<unreadable>")))))
(concat
(format "Symbol: %s\nKind: %s\n\n"
(symbol-name sym)
(if (string-empty-p kind) "symbol" kind))
(when fdoc
(format "Function doc:\n%s\n\n" (string-trim fdoc)))
(when vdoc
(format "Variable doc:\n%s\n\n" (string-trim vdoc)))
(when (and include-value is-var)
(format "Current value:\n%s\n" value-str))))))
(error
(format "Error describing symbol %s: %s"
symbol-name (error-message-string err)))))
Tool Definition
(gptel-make-tool
:name "describe_symbol"
:function #'gptel-elisp-describe-symbol
:description "Describe an Emacs symbol (function/variable/face), including docs and optional current value."
:args (list
'(:name "symbol_name" :type string :description "Name of the symbol to describe.")
'(:name "include_value" :type boolean :description "Include current variable value when symbol is bound." :optional t))
:category "emacs")
Example Interaction
Tool Calldescribe_symbolsymbol_name org-babel-map-src-blocks
▸
| symbol_name | org-babel-map-src-blocks |
Symbol: org-babel-map-src-blocks Kind: function Function doc: Evaluate BODY forms on each source-block in FILE. If FILE is nil evaluate BODY forms on source blocks in current buffer. During evaluation of BODY the following local variables are set relative to the currently matched code block. full-block ------- string holding the entirety of the code block beg-block -------- point at the beginning of the code block end-block -------- point at the end of the matched code block lang ------------- string holding the language of the code block beg-lang --------- point at the beginning of the lang end-lang --------- point at the end of the lang switches --------- string holding the switches beg-switches ----- point at the beginning of the switches end-switches ----- point at the end of the switches header-args ------ string holding the header-args beg-header-args -- point at the beginning of the header-args end-header-args -- point at the end of the header-args body ------------- string holding the body of the code block beg-body --------- point at the beginning of the body end-body --------- point at the end of the body (fn FILE &rest BODY)
eval_emacs_lisp
This is not something I turn on often, but it is easy enough to create. It allows the model to become fully able to iterate on its own environment. It usually breaks or does odd things. A little too claw'd out for my liking, within this environment.
So What?
Where systems of the moment like pi optimize for a minimal, reliable, mostly hands-off coding agent, I’m more interested in augmenting day-to-day thinking and documenting it as I go.
The orchestration is all up front, but it stays malleable and editable. You can document the interaction, reshape it, save it, come back later, or reuse it as context for future work.
A recurring complaint around tools like Claude Code is that harness behavior shifts over time or across updates. The system described above is almost orthogonal to that problem. The interaction model lives in my editor, and switching models is just another capability gptel already supports.
So if you like tinkering, and want a workspace you can slowly shape rather than a harness you have to adapt to, find (or build!) a malleable, cozy place to drink your tea, have your biscuits, and hone your own tools.