Cletus plugin kit: rules for coding agents, specification, host API, style guide, worker SDK and cletus-plugin tool

This commit is contained in:
Eidonix committed 2026-11-10 19:50:09 -05:00
commit 9b19d3a8be
41 files changed
+4121

No files matched your search

+134
View File
@@ -0,0 +1,134 @@
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"git.eidonix.com/eidonix/cletus-plugins/sdk/manifest"
)
type listFlag []string
func (l *listFlag) String() string { return strings.Join(*l, ",") }
func (l *listFlag) Set(v string) error { *l = append(*l, v); return nil }
func runRun(args []string) error {
fs := flag.NewFlagSet("run", flag.ContinueOnError)
files := fs.String("files", "testdata", "the folder the user opened")
workspace := fs.String("workspace", "", "a directory to serve as one shared folder")
edit := fs.Bool("edit", false, "the user allows edits")
timeout := fs.Duration("timeout", 2*time.Minute, "how long to wait for the answer")
var caps listFlag
fs.Var(&caps, "capability", "a Cletus feature the user has (repeatable)")
if err := fs.Parse(args); err != nil {
return err
}
rest := fs.Args()
if len(rest) == 0 {
return errors.New("run needs a command: tools, invoke, ui or service")
}
plugin, err := readPlugin(".")
if err != nil {
return err
}
wd, _ := os.Getwd()
filesDir, err := filepath.Abs(*files)
if err != nil {
return err
}
if err := os.MkdirAll(filesDir, 0o755); err != nil {
return err
}
wsDir := ""
if *workspace != "" {
if wsDir, err = filepath.Abs(*workspace); err != nil {
return err
}
}
bin, cleanup, err := buildWorker(wd)
if err != nil {
return err
}
defer cleanup()
host := &testHost{plugin: plugin, filesDir: filesDir, workspaceDir: wsDir, edit: *edit,
dataDir: filepath.Join(wd, ".cletus-dev", "data"), capabilities: map[string]bool{}, log: os.Stderr}
for _, c := range caps {
host.capabilities[c] = true
}
if err := host.start(bin); err != nil {
return err
}
defer host.stop()
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
arg := func(i int, fallback string) string {
if len(rest) > i {
return rest[i]
}
return fallback
}
var out json.RawMessage
switch rest[0] {
case "tools":
err = host.call(ctx, "tools.list", nil, &out)
case "invoke":
if len(rest) < 2 {
return errors.New("invoke needs a tool name")
}
err = host.call(ctx, "tools.invoke", map[string]any{"name": rest[1], "args": arg(2, "{}"), "working_dir": filesDir, "edit_mode": *edit}, &out)
case "ui":
if len(rest) < 2 {
return errors.New("ui needs a method name")
}
err = host.call(ctx, "ui."+strings.TrimPrefix(rest[1], "ui."), json.RawMessage(arg(2, "[]")), &out)
case "service":
if len(rest) < 2 {
return errors.New("service needs <service>.<method>")
}
err = host.call(ctx, "svc."+rest[1], map[string]any{"caller": "cletus-plugin", "mutating": false, "args": json.RawMessage(arg(2, "{}"))}, &out)
default:
return fmt.Errorf("unknown run command %q", rest[0])
}
if err != nil {
return err
}
var pretty bytes.Buffer
if json.Indent(&pretty, out, "", " ") != nil {
pretty.Write(out)
}
fmt.Println(pretty.String())
return nil
}
func readPlugin(dir string) (*manifest.Plugin, error) {
path := filepath.Join(dir, "plugin.json")
if _, err := os.Stat(path); err != nil {
return nil, manifest.ErrNoManifest
}
return manifest.Read(path)
}
// prefixWriter prefixes every line written to w.
type prefixWriter struct {
w io.Writer
prefix string
}
func (p prefixWriter) Write(b []byte) (int, error) {
for _, line := range strings.SplitAfter(string(b), "\n") {
if line != "" {
fmt.Fprint(p.w, p.prefix+line)
}
}
return len(b), nil
}