Evolving a Modular dev Experience in Emacs

2026-02-02 Mon 15:07 article llm

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
Existing Content in Buffer

What is SPC b b bound to?

Tool Call
describe_key
keysSPC 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)
Assistant Reply

SPC b b runs +vertico/switch-workspace-buffer, a Doom Emacs command for switching buffers within the current workspace. Typing a workspace number followed by space shows that workspace's buffer list instead.

Settings and presets

Screenshot 2026-02-04 at 11.28.24 AM.png

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.
CategoryUse Case
DiscoveryOrienting the conversation within the environment
Retrievalbringing in actual context
ResearchAccessing external sources to add to the context
ActionSide 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
User

Where should I implement a "read reddit" tool in my ~/.doom.d/local/gptel-tool-calls/ folder?

Tool Call
get_directory_tree
dir_path~/.doom.d/local/gptel-tool-calls/
max_depth2
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
Assistant

The structure groups tools by domain. A "read reddit" tool fits best in =gptel-web-utils.el= since it's web/API-related content fetching.

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
User

Given my recent emacs lisp buffers, what am I working on?

Tool Call
find_buffers_and_recent
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
Assistant

You’re working on Emacs Lisp tooling around gptel function/tool calls, specifically:

  • A local package/module: ~/.doom.d/local/gptel-tool-calls/
    • gptel-tool-calls.el (core orchestration)
    • gptel-file-utils.el
    • gptel-content-utils.el
    • gptel-web-utils.el
    • gptel-newpatch.el (likely patch/new behavior work)
  • You also recently touched upstream gptel-request.el, so you’re likely integrating or debugging against gptel internals/API behavior.

In short: you’re building/debugging a custom gptel tools layer (file/content/web utilities + call dispatch), with both local and remote Emacs configs in play.

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
User

find all skills markdown files in ~/src/openclaw related to productivity, and put them into categories

Tool Call
find_subdir_files
root_dir~/src/openclaw
patternskill.*\\.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
Assistant

Here are the tools/productivity-related skill markdown files in ~/src/openclaw, grouped by category:

Notes & Knowledge Management

  • skills/apple-notes/SKILL.md
  • skills/bear-notes/SKILL.md
  • skills/notion/SKILL.md
  • skills/obsidian/SKILL.md
  • extensions/feishu/skills/feishu-doc/SKILL.md
  • extensions/feishu/skills/feishu-wiki/SKILL.md

Tasks, Planning & Organization

  • skills/apple-reminders/SKILL.md
  • skills/things-mac/SKILL.md
  • skills/trello/SKILL.md

Communication & Collaboration

  • skills/slack/SKILL.md
  • skills/discord/SKILL.md
  • skills/imsg/SKILL.md
  • extensions/feishu/skills/feishu-drive/SKILL.md
  • extensions/feishu/skills/feishu-perm/SKILL.md

Developer / Terminal Productivity

  • skills/tmux/SKILL.md
  • skills/github/SKILL.md
  • skills/session-logs/SKILL.md
  • skills/skill-creator/SKILL.md
  • skills/clawhub/SKILL.md
  • skills/coding-agent/SKILL.md

Personal Data & Utilities

  • skills/1password/SKILL.md
  • skills/model-usage/SKILL.md
  • skills/summarize/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
User

what bash commands are commonly used for ~/src/openclaw/skills
consider they are usually bash code blocks in markdown files

Tool Call
search_path_rg
source~/src/openclaw/skills
pattern```bash|```sh
ctx5
~/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 ✂︎
------
Assistant

Based on the search results, here are the most commonly used bash commands in the ~/src/openclaw/skills markdown files:

Command-Line Tools

  • =curl= - API requests (Notion, Trello, weather, GitHub, OAuth)
  • =jq= - JSON parsing and filtering
  • =rg= (ripgrep) - Text searching
  • =git= - Version control operations
  • =gh= - GitHub CLI
  • =tmux= - Terminal multiplexing for persistent sessions

Skill-Specific CLIs

  • =openclaw= - OpenClaw system commands (nodes, system events)
  • =codex= - Coding agent operations
  • =claude= - Claude Code agent
  • =imsg= - iMessage CLI
  • =himalaya= - Email CLI
  • =grizzly= - Bear notes CLI
  • =peekaboo= - macOS automation
  • =gog gmail= - Gmail operations
  • =sag= - Audio generation (TTS)
  • =summarize= - Content summarization
  • =nano-pdf= - PDF editing

Common Patterns

  • Environment variables: =export VAR=value=, $VAR, $(cat file)
  • Piping: cat | tool, curl | jq, echo | command
  • Redirection: > file, >> file, << EOF (heredocs)
  • Loops: for f in files; do ... done
  • Temp directories: =SCRATCH=$(mktemp -d)=, TMPDIR
  • File operations: mkdir -p, ls -lh, head -1, cat
  • Background processes: bash background:true pty:true

Scripting Utilities

  • =uv= - Python environment management
  • =python3= / =python= - Running Python scripts
  • =npm= / =npx= - Node.js package management
  • =awk=, =sed=, =grep= - Text processing
  • =sleep=, =date= - Timing and timestamps

The pattern across skills emphasizes one-liners with pipes, JSON output + jq filtering, and dedicated CLI tools rather than raw bash scripting.

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
User

find and retrieve the definition for my color picker code in ~/.doom.d, and tell me how it works.

Tool Call
find_matching_lines
source~/.doom.d/local/config-files/macos.el
patterncolor
context_lines5
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)
Assistant

Perfect! I found the color picker code in your macOS configuration file. Let me retrieve the complete definition:

Tool Call
get_lines_range
source~/.doom.d/local/config-files/macos.el
start_line9
end_line37
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))
Assistant

Found it! Here's your color picker code from ~/.doom.d/local/config-files/macos.el:

This code defines:

  1. =nscolor2hex= - A helper function that converts macOS NSColor format to hex values
  2. =color-picker= - The main function that opens the macOS color picker dialog using AppleScript, gets the chosen color, converts it to hex, and either inserts it or passes it to a callback
  3. Key binding - Maps SPC o c (in normal/visual mode) to trigger the 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
User

Look for GitHub packages for emacs related to coffee but not coffeescript

Assistant

Let me search for Emacs packages related to coffee (but not coffeescript) on GitHub:

Tool Call
kagi_search
queryemacs 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
Assistant

Here are some interesting Emacs packages related to coffee (but not CoffeeScript):

Most interesting finds:

  1. roguel-ike - A "coffee-break roguelike" game for Emacs! Designed for short play sessions where you can create characters and do arena fights or dungeon explorations.
  2. coffee-theme - A warm color theme for GNU Emacs (makes sense for a coffee theme!)
  3. company-coffee - Adds coffee keywords to company-mode completion (though this one might actually be CoffeeScript-related based on the name)
  4. coffee.el - Part of emacs-goodies-el package
User

What does coffee.el do?

Tool Call
kagi_search
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
Assistant

Ha! That's amazing! 🎉

coffee.el is a joke/novelty package that implements the Hyper Text Coffee Pot Control Protocol (HTCPCP) from RFC2324 (an April Fools' RFC from 1998).

It allows you to:

  • Submit a "BREW" request to RFC2324-compliant coffee devices
  • Prompt the user for different coffee additives
  • Communicate with internet-connected coffee pots using HTCPCP

Essentially, it's Emacs's implementation of a protocol for controlling smart coffee makers over HTTP - complete with proper BREW requests! It's part of the grand tradition of humorous RFCs, but someone actually made it work in Emacs.

Some versions apparently require a special "BREW-capable" version of Emacs/W3 (the old Emacs web browser).

So it's not about CoffeeScript at all - it's literally about brewing coffee from Emacs! ☕

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-Type response header and handles different formats appropriately:
    • HTML: Uses Emacs's libxml and shr to 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
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
User

OK let's read the package readme for coffee.el

Tool Call
read_url
urlhttps://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.
Assistant

Here's the README from the dedicated coffee-el repository:

---

coffee.el -- Submit a BREW request to an RFC2324-compliant coffee device

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…

Requirements: coffee.el requires a special BREW-capable version of Emacs/W3 to be installed.

References:

  • 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 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
User

do a search for openclaw web page and grab the links from the top page.

Assistant

Great — I can do that.
I’ll run a web search for “openclaw web page”, open the top result, and extract its links.

Tool Call
kagi_search
queryopenclaw 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 }, }
Assistant

Top result is: *https://openclaw.ai/*

Tool Call
extract_page_links
urlhttps://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..
Assistant

Perfect! Extracted 129 links from the OpenClaw homepage.

Here's a sample of what we got:

Main Navigation:

Key Features Pages:

Press/Reviews:

  • MacStories article
  • StarryHope article

Community Testimonials:

  • 100+ Twitter/X links from users sharing their experiences
    • Setup/Hardware: Users running on Mac mini, Raspberry Pi, Windows — minimal tech friction
    • Integrations: Gmail, Calendar, WordPress, Hetzner, WHOOP, Todoist, Sentry (auto-test → fix → PR workflow), air purifier control
    • Workflows: Company ops, taxes, code reviews, PM pipelines, content creation, health reimbursements, appointment booking
    • Self-improvement: Claw sets up its own skills (e.g., Terminal CLI for flight search), proxies/copilots, and even cloning instances
    • Experience: "iPhone moment," "magic," "portal to a new reality," constant checking for new GitHub releases
    • Open/Hackable: Custom skills, Obsidian notes integration, local storage, no lock-in
    • 24/7 Autonomy: Background tasks, cron jobs, reminders, multi-instance Discord/Telegram homes

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_textnew_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
User

Add better output formatting to ~/weather.py

Assistant

I'll first read the current weather.py file to see what needs to be improved.

Tool Call
read_content
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 Call
patch
namefeat: add better output formatting
descriptionAdd formatted weather output with borders, columns, emoji icons, and color-coded temperature ranges for better readability
project_dir~
fileweather.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
Assistant
--- weather.py
+++ weather.py
@@ -12,6 +12,46 @@
-import os
-import sys
-import requests
-
-API_KEY = os.getenv("OPENWEATHER_API_KEY")
-BASE_URL = "https://api.openweathermap.org/data/2.5/weather"
+import os
+import sys
+import requests
+
+API_KEY = os.getenv("OPENWEATHER_API_KEY")
+BASE_URL = "https://api.openweathermap.org/data/2.5/weather"
+
+# Color codes for terminal output
+class Colors:
+    HOT = '\033[91m'      # Red
+    WARM = '\033[93m'     # Yellow
+    MILD = '\033[92m'     # Green
+    COLD = '\033[94m'     # Blue
+    FREEZING = '\033[96m' # Cyan
+    RESET = '\033[0m'
+
+def get_temp_color(temp_c: float) -> str:
+    """Return color code based on temperature."""
+    if temp_c >= 30:
+        return Colors.HOT
+    elif temp_c >= 20:
+        return Colors.WARM
+    elif temp_c >= 10:
+        return Colors.MILD
+    elif temp_c >= 0:
+        return Colors.COLD
+    else:
+        return Colors.FREEZING
+
+def get_weather_icon(description: str) -> str:
+    """Return emoji icon based on weather description."""
+    desc = description.lower()
+    if 'clear' in desc:
+        return '☀️'
+    elif 'cloud' in desc:
+        return '☁️'
+    elif 'rain' in desc or 'drizzle' in desc:
+        return '🌧️'
+    elif 'snow' in desc:
+        return '❄️'
+    elif 'thunder' in desc:
+        return '⛈️'
+    elif 'mist' in desc or 'fog' in desc:
+        return '🌫️'
+    else:
+        return '🌤️'
Tool Call
patch
namefeat: add formatted display function
descriptionAdd function to display weather data with nice formatting including borders and columns
project_dir~
fileweather.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
Assistant
--- weather.py
+++ weather.py
@@ -20,19 +20,54 @@
-def get_weather(city: str, api_key: str) -> dict:
-    params = {
-        "q": city,
-        "appid": api_key,
-        "units": "metric",  # Change to "imperial" for Fahrenheit
-    }
-    response = requests.get(BASE_URL, params=params, timeout=10)
-    response.raise_for_status()
-    data = response.json()
-
-    return {
-        "city": data.get("name", city),
-        "country": data.get("sys", {}).get("country", ""),
-        "temp_c": data.get("main", {}).get("temp"),
-        "feels_like_c": data.get("main", {}).get("feels_like"),
-        "humidity": data.get("main", {}).get("humidity"),
-        "description": (data.get("weather") or [{}])[0].get("description", ""),
-        "wind_mps": data.get("wind", {}).get("speed"),
-    }
+def get_weather(city: str, api_key: str) -> dict:
+    params = {
+        "q": city,
+        "appid": api_key,
+        "units": "metric",  # Change to "imperial" for Fahrenheit
+    }
+    response = requests.get(BASE_URL, params=params, timeout=10)
+    response.raise_for_status()
+    data = response.json()
+
+    return {
+        "city": data.get("name", city),
+        "country": data.get("sys", {}).get("country", ""),
+        "temp_c": data.get("main", {}).get("temp"),
+        "feels_like_c": data.get("main", {}).get("feels_like"),
+        "humidity": data.get("main", {}).get("humidity"),
+        "description": (data.get("weather") or [{}])[0].get("description", ""),
+        "wind_mps": data.get("wind", {}).get("speed"),
+    }
+
+def display_weather(weather: dict) -> None:
+    """Display weather data with formatted output."""
+    icon = get_weather_icon(weather['description'])
+    temp_color = get_temp_color(weather['temp_c'])
+    temp_f = weather['temp_c'] * 9/5 + 32
+    
+    # Build formatted output
+    border = "┌" + "─" * 50 + "┐"
+    separator = "├" + "─" * 50 + "┤"
+    empty = "│" + " " * 50 + "│"
+    
+    print(border)
+    print(f"│  {icon}  {weather['city']}, {weather['country']}" + " " * (42 - len(weather['city']) - len(weather['country'])) + "│")
+    print(separator)
+    
+    # Temperature line with color
+    temp_str = f"{temp_color}{weather['temp_c']:.1f}°C ({temp_f:.1f}°F){Colors.RESET}"
+    print(f"│  Temperature:     {temp_str}" + " " * (50 - 18 - len(f"{weather['temp_c']:.1f}°C ({temp_f:.1f}°F)")) + "│")
+    
+    # Feels like
+    feels_str = f"{temp_color}{weather['feels_like_c']:.1f}°C{Colors.RESET}"
+    print(f"│  Feels like:      {feels_str}" + " " * (50 - 18 - len(f"{weather['feels_like_c']:.1f}°C")) + "│")
+    
+    # Description
+    print(f"│  Condition:       {weather['description'].title()}" + " " * (50 - 18 - len(weather['description'].title())) + "│")
+    
+    # Humidity
+    print(f"│  Humidity:        {weather['humidity']}%" + " " * (50 - 18 - len(f"{weather['humidity']}%")) + "│")
+    
+    # Wind
+    print(f"│  Wind:            {weather['wind_mps']:.1f} m/s ({weather['wind_mps'] * 3.6:.1f} km/h)" + " " * (50 - 18 - len(f"{weather['wind_mps']:.1f} m/s ({weather['wind_mps'] * 3.6:.1f} km/h)")) + "│")
+    
+    print("└" + "─" * 50 + "┘")
+    print()
Tool Call
patch
namefeat: update main to use formatted display
descriptionUpdate main function to use the new formatted display function
project_dir~
fileweather.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
Assistant
--- weather.py
+++ weather.py
@@ -53,18 +53,13 @@
-    for city in cities:
-        try:
-            w = get_weather(city, API_KEY)
-            print(
-                f"{w['city']}, {w['country']}: "
-                f"{w['description'].title()}, "
-                f"{w['temp_c']}°C (feels like {w['feels_like_c']}°C), "
-                f"humidity {w['humidity']}%, wind {w['wind_mps']} m/s"
-            )
-        except requests.HTTPError as e:
-            if e.response is not None and e.response.status_code == 404:
-                print(f"{city}: city not found")
-            else:
-                print(f"{city}: HTTP error - {e}")
-        except requests.RequestException as e:
-            print(f"{city}: request failed - {e}")
-        except Exception as e:
-            print(f"{city}: unexpected error - {e}")
+    for city in cities:
+        try:
+            w = get_weather(city, API_KEY)
+            display_weather(w)
+        except requests.HTTPError as e:
+            if e.response is not None and e.response.status_code == 404:
+                print(f"❌ {city}: city not found\n")
+            else:
+                print(f"❌ {city}: HTTP error - {e}\n")
+        except requests.RequestException as e:
+            print(f"❌ {city}: request failed - {e}\n")
+        except Exception as e:
+            print(f"❌ {city}: unexpected error - {e}\n")
Assistant

Done! I've enhanced ~/weather.py with:

  • Boxed output with Unicode borders
  • Color-coded temperatures: 🔴 Hot (30°C+), 🟡 Warm (20-30°C), 🟢 Mild (10-20°C), 🔵 Cold (0-10°C), 🧊 Freezing (<0°C)
  • Weather icons: ☀️ ☁️ 🌧️ ❄️ ⛈️ 🌫️ based on conditions
  • Dual units: Celsius and Fahrenheit for temperature, m/s and km/h for wind
  • Aligned columns for clean presentation
  • Error indicators: ❌ emoji for failed requests

Run it with: python ~/weather.py London "New York" Tokyo

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
User

what elisp fn would I use to find all src blocks in an org mode file?

Assistant

I'll search for Emacs Lisp functions related to org-mode source blocks.

Tool Call
apropos
patternorg.*src.*block
typefunctions
limit20
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.
Assistant

Perfect! The key function you're looking for is org-babel-map-src-blocks. This function allows you to iterate over all source blocks in an org-mode file.

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
Assistant

Let me get more details:

Tool Call
describe_symbol
symbol_nameorg-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)
Assistant

So org-babel-map-src-blocks is the main function to find and iterate over all source blocks in an org-mode file.

Usage example:

(org-babel-map-src-blocks nil  ; nil = current buffer, or pass a filename
  (message "Found %s block at %d: %s" lang beg-block (substring body 0 (min 50 (length body)))))

This macro provides convenient local variables like lang, body, beg-block, end-block, etc. that you can use within the body forms.

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.