zot/examples/extensions/hello/main.go
patriceckhart 0c92d6e914 feat: extension system (subprocess + json-rpc, any language)
Phase 1: extensions can register slash commands and push chat
notifications. Tools and event subscriptions land in later phases.

Architecture: each extension is its own subprocess. Zot launches
it on startup, completes a hello/hello_ack handshake over its
stdin/stdout, then routes slash commands the extension registered.
Crash isolation, language agnostic, works with any executable
that can read/write json lines.

What lands here:

- internal/extproto: shared wire-format types (Frame, HelloFromExt,
  RegisterCommandFromExt, CommandResponseFromExt, NotifyFromExt,
  HelloAckFromHost, CommandInvokedFromHost, ShutdownFromHost...).
  Both the host and the SDK marshal/unmarshal the same types.

- internal/agent/extensions: discovery + lifecycle manager.
  - Discover() walks $ZOT_HOME/extensions and ./.zot/extensions
    (project-local first, global second; first wins for duplicates)
  - Spawns each enabled extension, captures stderr to
    $ZOT_HOME/logs/ext-<name>.log
  - Reads frames in a goroutine, dispatches register_command and
    notify, correlates command_response by id
  - Stop() sends shutdown, waits 2s, then SIGTERM/SIGKILL
  - HostHooks abstracts the tui callbacks (Notify/Submit/Insert/Display)

- Interactive bridge: extensions slot into the slash dispatcher
  *after* the built-in catalog, so built-ins always win on conflict.
  Extension-registered commands also flow into the autocomplete
  popup and /help via slashSuggester.SetExtra. NotifyFromExt frames
  render as muted [ext-name] notes above the editor.

- internal/agent/extcmd: `zot ext` CLI.
    list / install <path|git-url> / remove / enable / disable / logs

- pkg/zotext: public Go SDK. Construct an Extension, register
  Command(name, desc, fn), call Run(). Fn returns a Response built
  with Prompt(), Insert(), Display(), Noop(), or Errorf(). Stderr
  via Logf() so stdout stays clean for the protocol.

- examples/extensions/hello: working Go example registering /hello
  and /summon, plus README + extension.json.

- docs/extensions.md: full protocol reference, including a
  ~30-line raw-Python example for users who don't want the SDK.

Tests: internal/agent/extensions/manager_test.go spawns a mock
extension via /bin/sh and exercises the full handshake -> register
-> invoke -> response cycle. Verifies the hello frame ordering,
correlation-by-id, and graceful shutdown.

Verified manually: built and installed the example, drove it via
stdin pipes, confirmed clean handshake + correct frame ordering
and shutdown_ack. Builds vet-clean on darwin / linux / windows.

Editor.Insert exported (was Editor.insert) so the extension hooks
can drop text into the input.
2026-04-19 14:09:43 +02:00

41 lines
1.2 KiB
Go

// hello — a tiny zot extension that registers /hello and /summon.
//
// Build it:
//
// cd examples/extensions/hello
// go build -o hello .
//
// Then drop it next to its extension.json under
// $ZOT_HOME/extensions/hello/, or run `zot ext install ./hello`
// from this directory.
package main
import (
"strings"
"github.com/patriceckhart/zot/pkg/zotext"
)
func main() {
ext := zotext.New("hello", "1.0.0")
// /hello [name] — submits a friendly prompt to the agent.
ext.Command("hello", "say hello (optional name)", func(args string) zotext.Response {
who := strings.TrimSpace(args)
if who == "" {
return zotext.Prompt("Greet me with a short, slightly absurd compliment.")
}
return zotext.Prompt("Greet " + who + " with a short, slightly absurd compliment.")
})
// /summon — pushes a notice into the chat without involving the
// model. Useful for pretending we did something important.
ext.Command("summon", "show a tongue-in-cheek summon notice", func(args string) zotext.Response {
ext.Notify("info", "the daemon stirs in its cage.")
return zotext.Display("a wisp of incense curls past your terminal.")
})
if err := ext.Run(); err != nil {
ext.Logf("fatal: %v", err)
}
}