Navigation
Copy page

Lisp Extension Examples

On this page

Each section below is one self-contained extension you can drop into ~/.config/kli/extensions/, load with /reload, and use. They cover five contribution kinds: a tool the model can call, a slash command you run, a widget that draws under the prompt, a color theme, and a renderer that restyles a kind of transcript message. Pick the one that matches what you want to add.

Every example is a single .lisp file holding exactly one defextension. Single-file extensions load in the kli/author package, where defextension, command, reply, and rest-arg are available unqualified. Clause heads like tool, theme, and status-slot are matched by name, so they work unqualified too. Functions you call inside a clause that live in another package must be written with their package prefix; each example below shows the prefixes it needs. For the full grammar and the complete list of contribution kinds, see Contribution kinds and the Extension reference.

After writing any file, run /reload in your session, then /extensions to confirm it loaded enabled. If a file has an error, /reload warns about that one file and keeps every other extension working.

Add a tool the model can call

A tool is a function the model invokes during a turn. Declare it with a name, a description the model reads, a parameter schema, and a :runner. This one reverses a string.

~/.config/kli/extensions/reverse-tool.lisp:

lisp
(defextension reverse-tool
  (:provides
   (tool reverse
     :label "Reverse"
     :description "Reverse the characters of a string."
     :parameters '(:object (:text :string))
     :runner (lambda (tool parameters context &key call-id on-update)
               (declare (ignore tool context call-id on-update))
               (reverse (kli/ext:tool-parameter parameters :text))))))
  • :parameters is an :object schema. Each entry is (NAME :TYPE); add :optional t to make one optional, as in (:directory :string :optional t).
  • The runner signature (tool parameters context &key call-id on-update) is fixed. Declare-ignore what you do not use.
  • (kli/ext:tool-parameter parameters :text) reads one argument by name.
  • A runner may return a plain string, which kli wraps into a tool result. For a result with structured details or an error flag, return (kli/ext:make-tool-result :content (list (kli/ext:make-tool-text-content "...")) :error-p t).

After /reload, the reverse tool is in the model's tool set and the model can call it.

Add a slash command

A command runs when you type /name at the prompt. The handler returns a (reply ...) result. This one echoes the system clock.

~/.config/kli/extensions/now.lisp:

lisp
(defextension now
  (:provides
   (command "now"
     :description "Print the current time."
     :handler (lambda (command arguments context &key call-id on-update)
                (declare (ignore command arguments context call-id on-update))
                (multiple-value-bind (s m h) (get-decoded-time)
                  (reply (format nil "~2,'0D:~2,'0D:~2,'0D" h m s)))))))
  • The string after command is the name typed after the slash.
  • The handler signature (command arguments context &key call-id on-update) is fixed.
  • To read free text the user typed after the command name, add :arguments '(:tail :name) and call (rest-arg arguments), which returns the tail or nil.
  • (reply text) builds the result kli shows.

After /reload, /now prints the time.

Add a status-line widget

A widget draws lines in the footer under the prompt on every frame. Declare it with widget and a factory taking (protocol theme width) that returns a list of lines. This one shows the current working directory.

~/.config/kli/extensions/cwd-widget.lisp:

lisp
(defextension cwd-widget
  (:provides
   (widget cwd
     (lambda (protocol theme width)
       (declare (ignore protocol))
       (let ((text (format nil "cwd: ~A"
                           (uiop:getcwd))))
         (list (if theme
                   (kli/tui/style:style theme "muted"
                     (kli/text:pad-right text width))
                   (kli/text:pad-right text width))))))))
  • The factory returns a list of strings, one per footer line. Return nil for no lines.
  • width is the terminal width. (kli/text:pad-right text width) pads a line to fill it.
  • theme is the active theme, or nil when none is resolved. (kli/tui/style:style theme TOKEN text) colors text with a theme token such as "muted" or "accent". Guard the no-theme case as shown.
  • A widget that errors or returns a non-list contributes no lines and is isolated; the rest of the footer keeps drawing.

For a one-line value you set imperatively rather than recompute every frame, use a status-slot instead:

lisp
(defextension build-status
  (:provides
   (status-slot build :initial "build: idle")))

A slot registers a named footer segment seeded with :initial. Update it from a command or event handler with (kli/tui/status:set-status protocol :build "build: passing"), where protocol is (active-protocol context). An empty slot draws nothing.

After /reload, the footer shows the new line.

Add a theme

A theme is a named color palette. Declare it with theme and a theme value built from JSON by kli/tui/style:load-theme. The JSON has a name, a vars map of reusable color values, and a colors map from token names to either a hex color or a vars key. An empty token value means the terminal default.

~/.config/kli/extensions/solarized.lisp:

lisp
(defextension solarized
  (:provides
   (theme solarized
     (kli/tui/style:load-theme
      "{
         \"name\": \"solarized\",
         \"vars\": { \"base\": \"#268bd2\", \"red\": \"#dc322f\" },
         \"colors\": {
           \"accent\": \"base\",
           \"error\": \"red\",
           \"text\": \"\",
           \"mdHeading\": \"base\"
         }
       }"))))
  • load-theme accepts a JSON string, as here, or a pathname to a .json file.
  • The name in the JSON is how you select the theme; it does not have to match the defextension name.
  • Tokens you omit fall back to the active built-in palette. The full set of token names (such as accent, error, mdHeading, toolSuccessBg, syntaxKeyword) is in the Themes reference.

After /reload, the theme is registered and available to select.

Add a message renderer

A message renderer replaces how one kind of transcript event draws. Declare it with message-renderer, a transcript-event kind to key on, and a function taking (event theme width) that returns a list of lines. This one tags every assistant :message with a marker line above it.

~/.config/kli/extensions/reply-marker.lisp:

lisp
(defextension reply-marker
  (:provides
   (message-renderer :message
     (lambda (event theme width)
       (let* ((text (kli/tui/transcript:event-text event))
              (marker "<<< reply")
              (body (loop for line in (kli/text:wrap-text text width)
                          collect (kli/text:pad-right line width))))
         (cons (if theme
                   (kli/tui/style:style theme "accent"
                     (kli/text:pad-right marker width))
                   (kli/text:pad-right marker width))
               body))))))
  • The renderer keys on the event kind, :message here. It runs for every transcript event of that kind, replacing the default rendering for it.
  • (kli/tui/transcript:event-text event) reads the message text. event-role (:assistant, :user) and event-kind are also available; branch on event-role inside the function if you want to leave one role untouched by returning the default rendering for it.
  • Lines are strings sized to width. (kli/text:wrap-text text width) wraps long text, and kli/text:pad-right fills each line.

After /reload, assistant replies render with the marker.

The same kinds, in tree

These are small on purpose. The same kinds ship in kli's own builtins, where they do production work:

  • The tool kind: the builtin bash, filesystem, and lisp tool extensions.
  • The command kind: the builtin install and settings commands.
  • The theme kind: the builtin theme extension, with its :dark and :light palettes.
  • The widget, status-slot, and message-renderer kinds: contributed by the terminal-UI status and transcript subsystems.

For one extension that uses many kinds at once, read cairn: a manifest with a store-opening effect, its model tools, a live context hot-patch, and its own slash commands, each contribution carrying a retractor.

Next steps

  • The shared structure under all five — the defextension grammar, requirements, metadata, and the imperative kli-extension builder: see Anatomy of an extension.
  • Loading order, project-local extensions, and the enabled/disabled config: see Loading and managing extensions.
  • How a running kli installs and retracts these without a restart: see The live image.