mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
docs(ext): refresh examples and help text
Adds the todo panel example under examples/extensions, updates example manifests and READMEs to match the current extension API, and surfaces extension install/load commands in zot --help.
This commit is contained in:
parent
6b03aa3320
commit
a5fad05fa3
11 changed files with 464 additions and 23 deletions
|
|
@ -549,6 +549,8 @@ See:
|
|||
- `examples/extensions/weather/` — LLM-callable tool
|
||||
- `examples/extensions/guard/` — event subscriptions + tool-call
|
||||
interception (refuses dangerous bash patterns)
|
||||
- `examples/extensions/todo/` — interactive persistent panel + tool
|
||||
- `examples/extensions/scratchpad/` — source-run TypeScript commands + tool
|
||||
|
||||
### Hot reload
|
||||
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ authoring works too — rename `index.js` → `index.ts`, install
|
|||
|
||||
```json
|
||||
{
|
||||
"exec": "tsx",
|
||||
"args": ["index.ts"]
|
||||
"exec": "npx",
|
||||
"args": ["-y", "tsx", "./index.ts"]
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"version": "1.0.0",
|
||||
"exec": "node",
|
||||
"args": ["index.js"],
|
||||
"language": "typescript",
|
||||
"language": "javascript",
|
||||
"description": "registers /now and /uptime — a tiny time toolkit",
|
||||
"enabled": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# scratchpad — TypeScript extension example
|
||||
# scratchpad — example zot extension (TypeScript, source-run)
|
||||
|
||||
Real `.ts` (not `.js`), no build step, no SDK. Runs via `npx tsx`,
|
||||
which downloads itself into the npm cache on first invocation and
|
||||
runs from cache on every subsequent call.
|
||||
Real `.ts` (not `.js`), no build step, no SDK. Runs via `npx -y tsx
|
||||
./index.ts`, which downloads `tsx` into npm's cache on first
|
||||
invocation and then reuses the cached copy on subsequent runs.
|
||||
|
||||
Demonstrates:
|
||||
|
||||
|
|
@ -13,16 +13,7 @@ Demonstrates:
|
|||
|
||||
## Requirements
|
||||
|
||||
Node 18+ and `tsx` on `$PATH`:
|
||||
|
||||
```bash
|
||||
npm install -g tsx
|
||||
```
|
||||
|
||||
This is what `extension.json` invokes (`exec: "tsx"`). Without the
|
||||
global install, swap to `"exec": "npx"` + `"args": ["--yes", "tsx",
|
||||
"index.ts"]` — functional but adds ~1 second to each zot startup
|
||||
because npm checks the registry on every invocation.
|
||||
Node 18+ and `npx` (bundled with npm).
|
||||
|
||||
## Install
|
||||
|
||||
|
|
@ -32,6 +23,8 @@ From this directory:
|
|||
zot ext install .
|
||||
```
|
||||
|
||||
This copies the manifest + source into `$ZOT_HOME/extensions/scratchpad/`.
|
||||
|
||||
## Use
|
||||
|
||||
In zot:
|
||||
|
|
@ -65,8 +58,9 @@ without any infrastructure beyond `tsx`. If you want richer ergonomics
|
|||
|
||||
## See also
|
||||
|
||||
- `examples/extensions/clock` — JS sibling (no tsx required)
|
||||
- `examples/extensions/hello` — Go SDK
|
||||
- `examples/extensions/weather` — Go SDK, exposes one tool
|
||||
- `examples/extensions/guard` — Go SDK, demonstrates intercepts
|
||||
- `examples/extensions/clock` — JavaScript sibling (no build step)
|
||||
- `examples/extensions/hello` — Go SDK slash commands
|
||||
- `examples/extensions/weather` — Go SDK tool example
|
||||
- `examples/extensions/guard` — Go SDK intercept example
|
||||
- `examples/extensions/todo` — interactive panel example
|
||||
- `docs/extensions.md` — full protocol reference
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "scratchpad",
|
||||
"version": "1.0.0",
|
||||
"exec": "tsx",
|
||||
"args": ["index.ts"],
|
||||
"exec": "npx",
|
||||
"args": ["-y", "tsx", "./index.ts"],
|
||||
"language": "typescript",
|
||||
"description": "scratchpad notes (commands) + read_notes (tool) — written in plain .ts via tsx",
|
||||
"enabled": true
|
||||
|
|
|
|||
84
examples/extensions/todo/README.md
Normal file
84
examples/extensions/todo/README.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# todo — example zot extension (Go, interactive panel)
|
||||
|
||||
Demonstrates the interactive panel API plus a companion tool the model
|
||||
can call. The slash command opens a persistent todo panel; the tool lets
|
||||
zot read and update the same todo list.
|
||||
|
||||
## Requirements
|
||||
|
||||
Go 1.22+.
|
||||
|
||||
## Install
|
||||
|
||||
From this directory:
|
||||
|
||||
```bash
|
||||
zot ext install .
|
||||
```
|
||||
|
||||
The example is configured to run directly from source:
|
||||
|
||||
```json
|
||||
{
|
||||
"exec": "go",
|
||||
"args": ["run", "."]
|
||||
}
|
||||
```
|
||||
|
||||
That avoids architecture-specific binaries when sharing the example, as
|
||||
long as Go is installed.
|
||||
|
||||
## Optional local build
|
||||
|
||||
```bash
|
||||
cd examples/extensions/todo
|
||||
go build -o todo-panel .
|
||||
```
|
||||
|
||||
If you do build it, change `extension.json` back to:
|
||||
|
||||
```json
|
||||
{
|
||||
"exec": "./todo-panel"
|
||||
}
|
||||
```
|
||||
|
||||
## Use
|
||||
|
||||
In zot:
|
||||
|
||||
- `/todo` opens the panel
|
||||
|
||||
## Features
|
||||
|
||||
- `/todo` opens the panel
|
||||
- panel keys:
|
||||
- up/down - move
|
||||
- a - add with typed text
|
||||
- e - edit selected todo
|
||||
- x - toggle done
|
||||
- d - delete
|
||||
- r - redraw
|
||||
- esc - close panel
|
||||
- persistent storage in the extension data directory as `todos.json`
|
||||
- LLM tool: `todo_manage`
|
||||
- list
|
||||
- add
|
||||
- complete
|
||||
- edit
|
||||
- remove
|
||||
|
||||
## Natural-language examples
|
||||
|
||||
- Create a new entry named "Call Georg" on my to-do list.
|
||||
- Complete task "Call Georg".
|
||||
- Edit task "Call Georg" to "Call Georg tomorrow".
|
||||
- Remove task "Call Georg".
|
||||
|
||||
## See also
|
||||
|
||||
- `examples/extensions/hello` — Go SDK slash commands
|
||||
- `examples/extensions/weather` — Go SDK tool example
|
||||
- `examples/extensions/guard` — Go SDK intercept example
|
||||
- `examples/extensions/scratchpad` — TypeScript commands + tool
|
||||
- `docs/extensions.md` — full protocol reference
|
||||
9
examples/extensions/todo/extension.json
Normal file
9
examples/extensions/todo/extension.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "todo-panel",
|
||||
"version": "0.3.0",
|
||||
"exec": "go",
|
||||
"args": ["run", "."],
|
||||
"language": "go",
|
||||
"description": "persistent interactive todo panel",
|
||||
"enabled": true
|
||||
}
|
||||
7
examples/extensions/todo/go.mod
Normal file
7
examples/extensions/todo/go.mod
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
module zot-todo-extension
|
||||
|
||||
go 1.22
|
||||
|
||||
require github.com/patriceckhart/zot v0.0.0
|
||||
|
||||
replace github.com/patriceckhart/zot => /Users/pat/Developer/zot
|
||||
336
examples/extensions/todo/main.go
Normal file
336
examples/extensions/todo/main.go
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/patriceckhart/zot/pkg/zotext"
|
||||
)
|
||||
|
||||
type todoItem struct {
|
||||
Text string `json:"text"`
|
||||
Done bool `json:"done"`
|
||||
}
|
||||
|
||||
type store struct {
|
||||
Items []todoItem `json:"items"`
|
||||
Cursor int `json:"cursor"`
|
||||
}
|
||||
|
||||
type toolArgs struct {
|
||||
Action string `json:"action"`
|
||||
Title string `json:"title"`
|
||||
NewTitle string `json:"new_title"`
|
||||
}
|
||||
|
||||
type app struct {
|
||||
ext *zotext.Extension
|
||||
mu sync.Mutex
|
||||
path string
|
||||
st store
|
||||
mode string
|
||||
edit string
|
||||
}
|
||||
|
||||
const panelID = "todos-main"
|
||||
|
||||
func main() {
|
||||
ext := zotext.New("todo-panel", "0.3.0")
|
||||
a := &app{ext: ext}
|
||||
ext.Command("todo", "open a persistent todo panel", func(args string) zotext.Response {
|
||||
if err := a.ensureLoaded(); err != nil {
|
||||
return zotext.Errorf("load todos: %v", err)
|
||||
}
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
return zotext.OpenPanel(panelID, a.title(), a.renderLines(), a.footer())
|
||||
})
|
||||
schema, _ := json.Marshal(map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"action": map[string]any{"type": "string", "enum": []string{"list", "add", "complete", "edit", "remove"}},
|
||||
"title": map[string]any{"type": "string"},
|
||||
"new_title": map[string]any{"type": "string"},
|
||||
},
|
||||
"required": []string{"action"},
|
||||
})
|
||||
ext.Tool("todo_manage", "List, add, complete, edit, or remove todos by title.", schema, a.handleTool)
|
||||
ext.OnPanelKey(panelID, a.handleKey, nil)
|
||||
if err := ext.Run(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *app) ensureLoaded() error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.path != "" {
|
||||
return nil
|
||||
}
|
||||
dir := a.ext.Host().DataDir
|
||||
if dir == "" {
|
||||
dir = a.ext.Host().ExtensionDir
|
||||
}
|
||||
if dir == "" {
|
||||
return fmt.Errorf("host did not provide extension_dir/data_dir")
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
a.path = filepath.Join(dir, "todos.json")
|
||||
b, err := os.ReadFile(a.path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
a.st = store{Items: []todoItem{{Text: "Build something fun", Done: false}}}
|
||||
return a.saveLocked()
|
||||
}
|
||||
return err
|
||||
}
|
||||
if len(b) == 0 {
|
||||
a.st = store{}
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(b, &a.st); err != nil {
|
||||
return err
|
||||
}
|
||||
a.clampLocked()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *app) saveLocked() error {
|
||||
a.clampLocked()
|
||||
b, err := json.MarshalIndent(a.st, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(a.path, b, 0o644)
|
||||
}
|
||||
|
||||
func (a *app) clampLocked() {
|
||||
if len(a.st.Items) == 0 {
|
||||
a.st.Cursor = 0
|
||||
return
|
||||
}
|
||||
if a.st.Cursor < 0 {
|
||||
a.st.Cursor = 0
|
||||
}
|
||||
if a.st.Cursor >= len(a.st.Items) {
|
||||
a.st.Cursor = len(a.st.Items) - 1
|
||||
}
|
||||
}
|
||||
|
||||
func (a *app) title() string {
|
||||
done := 0
|
||||
for _, it := range a.st.Items {
|
||||
if it.Done {
|
||||
done++
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("Todos (%d/%d done)", done, len(a.st.Items))
|
||||
}
|
||||
|
||||
func (a *app) renderLines() []string {
|
||||
if a.mode == "add" || a.mode == "edit" {
|
||||
label := "Add todo"
|
||||
if a.mode == "edit" {
|
||||
label = "Edit todo"
|
||||
}
|
||||
return []string{
|
||||
" " + label,
|
||||
"",
|
||||
" " + a.edit + "▌",
|
||||
"",
|
||||
" Enter saves, Esc cancels",
|
||||
}
|
||||
}
|
||||
lines := []string{}
|
||||
if len(a.st.Items) == 0 {
|
||||
return []string{" No todos yet.", "", " Press a to add one."}
|
||||
}
|
||||
for i, it := range a.st.Items {
|
||||
cursor := " "
|
||||
if i == a.st.Cursor {
|
||||
cursor = "› "
|
||||
}
|
||||
box := "□"
|
||||
if it.Done {
|
||||
box = "✓"
|
||||
}
|
||||
text := it.Text
|
||||
if it.Done {
|
||||
text += " (done)"
|
||||
}
|
||||
lines = append(lines, cursor+box+" "+text)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func (a *app) footer() string {
|
||||
if a.mode == "add" || a.mode == "edit" {
|
||||
return "type text - enter save - esc cancel - backspace delete"
|
||||
}
|
||||
return "↑/↓ navigate - x toggle - a add - e edit - d delete - r refresh - esc close"
|
||||
}
|
||||
|
||||
func (a *app) rerenderLocked() {
|
||||
a.ext.RenderPanel(panelID, a.title(), a.renderLines(), a.footer())
|
||||
}
|
||||
|
||||
func (a *app) handleKey(key, text string) {
|
||||
if err := a.ensureLoaded(); err != nil {
|
||||
a.ext.Notify("error", fmt.Sprintf("todo: %v", err))
|
||||
return
|
||||
}
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.mode == "add" || a.mode == "edit" {
|
||||
a.handleEditModeLocked(key, text)
|
||||
return
|
||||
}
|
||||
switch key {
|
||||
case "up":
|
||||
a.st.Cursor--
|
||||
case "down":
|
||||
a.st.Cursor++
|
||||
case "rune":
|
||||
switch strings.ToLower(text) {
|
||||
case "x":
|
||||
if len(a.st.Items) > 0 {
|
||||
a.st.Items[a.st.Cursor].Done = !a.st.Items[a.st.Cursor].Done
|
||||
}
|
||||
case "d":
|
||||
if len(a.st.Items) > 0 {
|
||||
a.st.Items = append(a.st.Items[:a.st.Cursor], a.st.Items[a.st.Cursor+1:]...)
|
||||
}
|
||||
case "a":
|
||||
a.mode = "add"
|
||||
a.edit = ""
|
||||
case "e":
|
||||
if len(a.st.Items) > 0 {
|
||||
a.mode = "edit"
|
||||
a.edit = a.st.Items[a.st.Cursor].Text
|
||||
}
|
||||
case "r":
|
||||
}
|
||||
}
|
||||
a.clampLocked()
|
||||
if err := a.saveLocked(); err != nil {
|
||||
a.ext.Notify("error", fmt.Sprintf("save todos: %v", err))
|
||||
}
|
||||
a.rerenderLocked()
|
||||
}
|
||||
|
||||
func (a *app) handleEditModeLocked(key, text string) {
|
||||
switch key {
|
||||
case "enter":
|
||||
trimmed := strings.TrimSpace(a.edit)
|
||||
if trimmed != "" {
|
||||
if a.mode == "add" {
|
||||
a.st.Items = append(a.st.Items, todoItem{Text: trimmed})
|
||||
a.st.Cursor = len(a.st.Items) - 1
|
||||
} else if a.mode == "edit" && len(a.st.Items) > 0 {
|
||||
a.st.Items[a.st.Cursor].Text = trimmed
|
||||
}
|
||||
}
|
||||
a.mode = ""
|
||||
a.edit = ""
|
||||
case "backspace":
|
||||
if len(a.edit) > 0 {
|
||||
r := []rune(a.edit)
|
||||
a.edit = string(r[:len(r)-1])
|
||||
}
|
||||
case "esc":
|
||||
a.mode = ""
|
||||
a.edit = ""
|
||||
case "rune":
|
||||
if text != "" {
|
||||
a.edit += text
|
||||
}
|
||||
}
|
||||
if err := a.saveLocked(); err != nil {
|
||||
a.ext.Notify("error", fmt.Sprintf("save todos: %v", err))
|
||||
}
|
||||
a.rerenderLocked()
|
||||
}
|
||||
|
||||
func (a *app) handleTool(args json.RawMessage) zotext.ToolResult {
|
||||
if err := a.ensureLoaded(); err != nil {
|
||||
return zotext.TextErrorResult("load todos: " + err.Error())
|
||||
}
|
||||
var in toolArgs
|
||||
if err := json.Unmarshal(args, &in); err != nil {
|
||||
return zotext.TextErrorResult("invalid args: " + err.Error())
|
||||
}
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
switch in.Action {
|
||||
case "list":
|
||||
if len(a.st.Items) == 0 {
|
||||
return zotext.TextResult("No todos.")
|
||||
}
|
||||
var lines []string
|
||||
for _, it := range a.st.Items {
|
||||
mark := "[ ]"
|
||||
if it.Done {
|
||||
mark = "[x]"
|
||||
}
|
||||
lines = append(lines, mark+" "+it.Text)
|
||||
}
|
||||
return zotext.TextResult(strings.Join(lines, "\n"))
|
||||
case "add":
|
||||
if strings.TrimSpace(in.Title) == "" {
|
||||
return zotext.TextErrorResult("title is required for add")
|
||||
}
|
||||
a.st.Items = append(a.st.Items, todoItem{Text: strings.TrimSpace(in.Title)})
|
||||
a.st.Cursor = len(a.st.Items) - 1
|
||||
case "complete":
|
||||
idx := a.findByTitleLocked(in.Title)
|
||||
if idx < 0 {
|
||||
return zotext.TextErrorResult("todo not found: " + in.Title)
|
||||
}
|
||||
a.st.Items[idx].Done = true
|
||||
a.st.Cursor = idx
|
||||
case "edit":
|
||||
idx := a.findByTitleLocked(in.Title)
|
||||
if idx < 0 {
|
||||
return zotext.TextErrorResult("todo not found: " + in.Title)
|
||||
}
|
||||
if strings.TrimSpace(in.NewTitle) == "" {
|
||||
return zotext.TextErrorResult("new_title is required for edit")
|
||||
}
|
||||
a.st.Items[idx].Text = strings.TrimSpace(in.NewTitle)
|
||||
a.st.Cursor = idx
|
||||
case "remove":
|
||||
idx := a.findByTitleLocked(in.Title)
|
||||
if idx < 0 {
|
||||
return zotext.TextErrorResult("todo not found: " + in.Title)
|
||||
}
|
||||
a.st.Items = append(a.st.Items[:idx], a.st.Items[idx+1:]...)
|
||||
case "":
|
||||
return zotext.TextErrorResult("action is required")
|
||||
default:
|
||||
return zotext.TextErrorResult("unsupported action: " + in.Action)
|
||||
}
|
||||
if err := a.saveLocked(); err != nil {
|
||||
return zotext.TextErrorResult("save todos: " + err.Error())
|
||||
}
|
||||
if a.mode == "" {
|
||||
a.rerenderLocked()
|
||||
}
|
||||
return zotext.TextResult("ok")
|
||||
}
|
||||
|
||||
func (a *app) findByTitleLocked(title string) int {
|
||||
needle := strings.TrimSpace(strings.ToLower(title))
|
||||
for i, it := range a.st.Items {
|
||||
if strings.TrimSpace(strings.ToLower(it.Text)) == needle {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
BIN
examples/extensions/todo/zot-todo-extension
Executable file
BIN
examples/extensions/todo/zot-todo-extension
Executable file
Binary file not shown.
|
|
@ -248,6 +248,9 @@ usage:
|
|||
zot -p "prompt" print final text, exit
|
||||
zot --json "prompt" newline-delimited json events, exit
|
||||
zot rpc json-rpc loop on stdin/stdout (see docs/rpc.md)
|
||||
zot ext help extension manager help
|
||||
zot ext list list installed extensions
|
||||
zot ext install <path|url> install an extension into $ZOT_HOME/extensions/
|
||||
zot telegram-bot setup configure a telegram bot (from BotFather)
|
||||
zot telegram-bot run foreground bridge (ctrl+c to stop)
|
||||
zot telegram-bot start background bridge (detached)
|
||||
|
|
@ -297,5 +300,11 @@ flags:
|
|||
--list-models print known models and exit
|
||||
-h, --help this message
|
||||
-v, --version version info
|
||||
|
||||
extensions:
|
||||
zot ext install ./path/to/ext install a local extension
|
||||
zot ext install https://...git clone + install from git
|
||||
zot --ext ./path/to/ext load an extension for this run only
|
||||
zot ext help show all extension subcommands
|
||||
`, version)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue