# Lisp Extension Examples


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](/kli/extend/lisp-extensions/contribution-kinds) and the [Extension reference](/kli/extend/lisp-extensions/anatomy).

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](/kli/commands/themes).

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](/kli/extend/lisp-extensions/anatomy).
- Loading order, project-local extensions, and the `enabled`/`disabled` config: see [Loading and managing extensions](/kli/extend/lisp-extensions/loading-and-managing).
- How a running kli installs and retracts these without a restart: see [The live image](/kli/concepts/the-live-image).
