Cletus plugin kit: rules for coding agents, specification, host API, style guide, worker SDK and cletus-plugin tool
This commit is contained in:
commit
9b19d3a8be
41 files changed
+4121
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user