Navigation
Copy page

Contribution Kinds

On this page

An extension is a bundle of contributions. Each contribution is one clause inside a (:provides ...) block, and each one installs when the extension loads and retracts when it unloads. This page shows the clause for each kind you are likely to write, and what its retraction undoes. Pick the kind that matches what you want to add.

Every clause shown here goes in a defextension:

lisp
(defextension my-extension
  (:provides
   ;; one or more contribution clauses
   ))

A single-file extension loads in the kli/author package, so the clause heads below (command, on, tool, and the rest) are available unqualified. After editing a file, run /reload to retract and reinstall. For the surrounding mechanics, see Write your first Lisp extension; for the complete grammar and every clause, see Extension reference.

Requirements are derived from the clauses you write. A command clause, for example, derives the requirement for the commands capability, so an extension that needs nothing beyond what its clauses imply omits :requires entirely.

Add a slash command

A command clause adds a /name the user can type.

lisp
(command "greet"
  :description "Greet someone by name."
  :arguments '(:tail :name)
  :handler (lambda (command arguments context &key call-id on-update)
             (declare (ignore command context call-id on-update))
             (reply (format nil "Hello ~A!"
                            (or (rest-arg arguments) "world")))))

The handler signature is fixed: (command arguments context &key call-id on-update). (reply text) builds the result shown to the user; (rest-arg arguments) returns the free-text tail after the command name, or nil. Keys you can pass: :label, :description, :arguments, :handler, :completer, :metadata.

Retraction unregisters the command from the per-session command provider. After unload, typing /greet does nothing.

Add an event handler

An on clause runs a function each time an event of a given type is dispatched.

lisp
(on :tool/call
    (lambda (event context)
      (declare (ignore event))
      (notify context "A tool ran." :level :info)))

The handler signature is (event context). (notify context text :level :info) surfaces text to the user as a notification, and is a no-op when no event provider is installed. Handlers for the same event type fire in install order.

An event handler carries no requirement of its own: it sits inert in per-session handler storage until the event system dispatches its type. Retraction removes the handler from that storage, so the function stops firing.

Add a tool

A tool clause adds a tool the model can call during the agent loop.

lisp
(tool word-count
  :label "Word Count"
  :description "Count the words in a string."
  :parameters '(:object (:text :string))
  :runner #'my-word-count
  :metadata '())

The runner signature is (tool parameters context &key call-id on-update) and returns a tool result. Keys: :label, :description, :parameters, :runner, :renderer, :metadata. Use :metadata '(:capabilities (...)) to declare the capabilities the tool needs, and :renderer to control how its result appears in the transcript.

The tool registers as a live object in the session and joins the set the model sees. Retraction removes the live object and drops the tool from that set, so the model can no longer call it.

Bind a key

A keybinding clause maps a key to an editor action in the terminal UI.

lisp
(keybinding "ctrl+x" :clear-screen)

The first argument is the key id, a string like "ctrl+l", "alt+b", or "enter". The second is the action keyword it should run, drawn from the editor's action set (for example :clear-screen, :move-word-left, :delete-word-backward, :undo). Installing a binding records whatever action that key previously held.

Retraction restores the previous binding if the key had one, or unbinds the key if it did not. When two extensions both touch a key, retraction unwinds in reverse, so the original binding returns.

Add a theme

A theme clause registers a named theme the user can switch to.

lisp
(theme "solarized"
  (kli/tui/style:load-theme #p"~/.config/kli/themes/solarized.json"))

The first argument is the theme name; the second is a theme value. kli/tui/style:load-theme reads a theme from a pathname, a JSON string, or a parsed object, resolving its color tokens once at load time. Registering a theme does not make it active; the user selects it.

Retraction unregisters the theme by name. After unload it no longer appears in the list of available themes.

Add a status slot

A status-slot clause reserves a named slot in the status line that your code can write to.

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

The first argument is the slot id; :initial sets the starting text (default empty). The slot holds whatever text you put in it and renders in the status line.

Retraction unregisters the slot, removing it from the status line.

Render a message kind

A message-renderer clause controls how a transcript event of a given kind is drawn.

lisp
(message-renderer :build/progress
  (lambda (event theme width)
    (declare (ignore width))
    (list (kli/tui/style:style
           theme "accent"
           (format nil "build: ~A"
                   (getf (kli/event:event-payload event) :stage))))))

The first argument is the event kind; the second is a function (event theme width) returning the lines to draw. (kli/tui/style:style theme token text) colors a span by a theme token; (kli/event:event-payload event) reads the event's payload plist. A renderer is a method keyed on the event kind, so it takes effect only for events of that kind.

Retraction removes the method, and the kind falls back to default rendering.

Declare a settings subtree

A settings clause declares your extension's subtree of the top-level extensions object in settings.json: the key names it accepts and the schema each value must satisfy.

lisp
(settings my-extension
  (:object
   ("greeting" (:string :default "hello"))
   ("retries" (:integer :min 0 :max 5))
   ("mode" (:enum ("fast" "careful") :default "fast"))))

The clause is (settings NAME SCHEMA). A symbol NAME downcases to the JSON key (my-extension above owns extensions.my-extension); pass a string when the key needs exact casing. The schema is quoted data:

Spec Accepts
(:object ("key" SPEC) ...) An object with the listed keys, each validated by its spec. Keys not listed diagnose as unknown.
(:string) A string.
(:boolean) true or false.
(:integer :min N :max M) An integer, optionally bounded.
(:number :min N :max M) A number, optionally bounded.
(:enum ("a" "b")) One of the listed strings.
(:or SPEC SPEC ...) A value matching any alternative.

Every leaf takes :default, and every declared key is optional in the files. Read values back with (kli/config:declared-settings-value context "my-extension" "retries"); it returns the configured value, or the declared default when the files omit the key, and a second value saying which (:settings, :default, or nil for neither).

Declaring buys three things. The subtree is validated when your extension activates, and every mismatch warns with the exact path — boot diagnostics the user sees instead of a silently ignored key. /settings lists the subtree, whether the files carry it, and its current diagnostics. And the subtree tiers like every built-in key: global under project under profile overlay, deep-merged key by key. See settings.json for the user-side view.

A malformed schema signals when the extension loads — that is an authoring error. A malformed value in the user's files only warns: settings never break boot. And declaration describes, never grants — no key in your subtree can confer authority; what your tools may do is governed by capabilities alone.

Retraction removes the declaration from the registry. The JSON stays in the user's files; /settings then lists that subtree as undeclared.

Anything else: an effect

When no kind above fits, an effect clause runs arbitrary paired install-and-revert logic. This is the general escape hatch: you write both halves, and you own the symmetry.

lisp
(effect mirror-log
  ;; installer
  (lambda (protocol contribution context)
    (declare (ignore protocol contribution context))
    (open #p"/tmp/kli-mirror.log" :direction :output
                                  :if-exists :append
                                  :if-does-not-exist :create))
  ;; retractor
  (lambda (protocol contribution context)
    (declare (ignore protocol context))
    (close (kli/ext:contribution-state contribution))))

The clause is positional: (effect NAME installer retractor). Both functions take (protocol contribution context). The installer's return value is stored as the contribution's state; the retractor reads it back with (kli/ext:contribution-state contribution) to undo exactly what was installed. The example stores the open stream on install and closes it on retract.

The retractor is required, because retraction must drain whatever install created: unregister what was registered, restore what was replaced, close what was opened. When an effect genuinely has nothing to undo, pass :no-op as the retractor rather than omitting it.

lisp
(effect announce
  (lambda (protocol contribution context)
    (declare (ignore protocol contribution))
    (notify context "Extension loaded." :level :info))
  :no-op)

State that must survive a turn but die with the extension belongs in protocol storage, not in a global. Reach it with (kli/ext:ensure-protocol-storage protocol KEY constructor); a global outlives retraction and leaks across reloads.

The clause heads above are matched by name, so they work unqualified from any package. The helper functions you call inside a clause are different: a single-file extension gets only the easy-tier names unqualified (defextension, kli-extension, command, on, reply, rest-arg, notify). Everything else, including the kli/ext, kli/event, and kli/tui/style names shown here, must be package-qualified.

These kinds in tree

The clauses above are minimal on purpose. Every kind also ships in kli's own builtin extensions, where you can read it doing production work:

  • Tool — the builtin bash, filesystem, and lisp tool extensions.
  • Slash command — the builtin install, settings, and profile commands.
  • Theme — the builtin theme extension registers the :dark and :light themes with (theme :dark (load-theme ...)).
  • Effect — the builtin prompt-templates and skills extensions, and cairn, each register commands or open state through an effect with a paired retractor.
  • Keybinding, status slot, widget, message renderer — defined and contributed by the keymap and terminal-UI subsystems.

cairn is a full external extension to read end to end: one manifest with a store-opening effect, its model tools, a live context hot-patch, and its own slash commands, each contribution carrying a retractor. To add a kind that none of these cover, see Defining a contribution kind.

Next