change tools to hierarchical

This commit is contained in:
Levi Neely 2026-04-30 14:38:47 +02:00
parent 7c113c1db4
commit 9bfccebff6
2 changed files with 75 additions and 15 deletions

View File

@ -9,6 +9,7 @@ import (
"io" "io"
"net" "net"
"os" "os"
"path/filepath"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -102,12 +103,20 @@ func (s *ExecStore) Open(name string) (StoreEntry, error) {
// ToolStore is a BlobStore backed by the tools directory. // ToolStore is a BlobStore backed by the tools directory.
// It extends FlatDirStore with a synthetic "idx" entry that lists all tools // It extends FlatDirStore with a synthetic "idx" entry that lists all tools
// with their descriptions and argument signatures. // with their descriptions and argument signatures.
// It supports subdirectories (e.g. _lib/) for shared library code.
type ToolStore struct { type ToolStore struct {
FlatDirStore FlatDirStore
dir string
} }
func NewToolStore() *ToolStore { func NewToolStore() *ToolStore {
return &ToolStore{FlatDirStore: NewFlatDirStore(execute.ToolsPath(), 0755)} dir := execute.ToolsPath()
return &ToolStore{FlatDirStore: NewFlatDirStore(dir, 0755), dir: dir}
}
// ListDir lists entries in a subdirectory relative to the tools root.
func (s *ToolStore) ListDir(rel string) ([]os.DirEntry, error) {
return os.ReadDir(filepath.Join(s.dir, rel))
} }
func (s *ToolStore) Stat(name string) (os.FileInfo, error) { func (s *ToolStore) Stat(name string) (os.FileInfo, error) {
@ -210,7 +219,7 @@ type Server struct {
agentStore Store agentStore Store
promptStore Store promptStore Store
memStore Store memStore Store
toolStore Store toolStore *ToolStore
utilStore Store utilStore Store
pluginStore Store pluginStore Store
skillStore Store skillStore Store
@ -566,7 +575,17 @@ func (s *Server) pathType(path string) string {
return "file" return "file"
} }
case len(parts) == 2 && parts[0] == "t": case len(parts) == 2 && parts[0] == "t":
if _, err := s.toolStore.Stat(parts[1]); err == nil { if info, err := s.toolStore.Stat(parts[1]); err == nil {
if info.IsDir() {
return "dir"
}
return "file"
}
case len(parts) == 3 && parts[0] == "t":
if info, err := s.toolStore.Stat(parts[1] + "/" + parts[2]); err == nil {
if info.IsDir() {
return "dir"
}
return "file" return "file"
} }
case len(parts) == 3 && parts[0] == "s": case len(parts) == 3 && parts[0] == "s":
@ -605,6 +624,13 @@ func pathBase(path string) string {
return path[i+1:] return path[i+1:]
} }
func boolToDir(isDir bool) uint32 {
if isDir {
return uint32(plan9.DMDIR)
}
return 0
}
func (s *Server) attach(cs *connState, fc *plan9.Fcall) *plan9.Fcall { func (s *Server) attach(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
cs.mu.Lock() cs.mu.Lock()
defer cs.mu.Unlock() defer cs.mu.Unlock()
@ -691,6 +717,24 @@ func (s *Server) create(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
return errFcall(fc, "bad fid") return errFcall(fc, "bad fid")
} }
if fc.Perm&plan9.DMDIR != 0 { if fc.Perm&plan9.DMDIR != 0 {
if f.path == "/t" || strings.HasPrefix(f.path, "/t/") {
rel := strings.TrimPrefix(f.path, "/t")
if rel == "" {
rel = fc.Name
} else {
rel = rel[1:] + "/" + fc.Name // strip leading /
}
dir := filepath.Join(s.toolStore.dir, rel)
if err := os.MkdirAll(dir, 0755); err != nil {
return errFcall(fc, err.Error())
}
newPath := pathJoin(f.path, fc.Name)
qid := plan9.Qid{Type: QTDir, Path: qidPath(newPath)}
f.path = newPath
f.qid = qid
f.mode = fc.Mode
return &plan9.Fcall{Type: plan9.Rcreate, Tag: fc.Tag, Qid: qid}
}
return errFcall(fc, "mkdir not supported") return errFcall(fc, "mkdir not supported")
} }
@ -718,6 +762,13 @@ func (s *Server) create(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
if err := s.skillStore.Create(fc.Name); err != nil { if err := s.skillStore.Create(fc.Name); err != nil {
return errFcall(fc, err.Error()) return errFcall(fc, err.Error())
} }
default:
if strings.HasPrefix(f.path, "/t/") {
rel := strings.TrimPrefix(f.path, "/t/") + "/" + fc.Name
if err := s.toolStore.Create(rel); err != nil {
return errFcall(fc, err.Error())
}
}
} }
qid := plan9.Qid{Path: qidPath(newPath)} qid := plan9.Qid{Path: qidPath(newPath)}
@ -818,7 +869,7 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall, ctx context.Context) *plan
// Tool files are served from the tool store. // Tool files are served from the tool store.
if strings.HasPrefix(path, "/t/") { if strings.HasPrefix(path, "/t/") {
s.log.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count) s.log.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
content, err := storeReadCtx(ctx, s.toolStore, pathBase(path)) content, err := storeReadCtx(ctx, s.toolStore, strings.TrimPrefix(path, "/t/"))
if err != nil { if err != nil {
return errFcall(fc, err.Error()) return errFcall(fc, err.Error())
} }
@ -1029,11 +1080,14 @@ func (s *Server) wstat(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
cs.mu.Unlock() cs.mu.Unlock()
case strings.HasPrefix(f.path, "/t/"): case strings.HasPrefix(f.path, "/t/"):
if err := s.toolStore.Rename(oldName, newDir.Name); err != nil { oldRel := strings.TrimPrefix(f.path, "/t/")
parentRel := oldRel[:len(oldRel)-len(oldName)]
newRel := parentRel + newDir.Name
if err := s.toolStore.Rename(oldRel, newRel); err != nil {
return errFcall(fc, err.Error()) return errFcall(fc, err.Error())
} }
cs.mu.Lock() cs.mu.Lock()
f.path = "/t/" + newDir.Name f.path = "/t/" + newRel
f.qid.Path = qidPath(f.path) f.qid.Path = qidPath(f.path)
cs.mu.Unlock() cs.mu.Unlock()
@ -1120,7 +1174,7 @@ func (s *Server) remove(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
case strings.HasPrefix(path, "/sk/"): case strings.HasPrefix(path, "/sk/"):
err = s.skillStore.Delete(pathBase(path)) err = s.skillStore.Delete(pathBase(path))
case strings.HasPrefix(path, "/t/"): case strings.HasPrefix(path, "/t/"):
err = s.toolStore.Delete(pathBase(path)) err = s.toolStore.Delete(strings.TrimPrefix(path, "/t/"))
case strings.HasPrefix(path, "/tmp/"): case strings.HasPrefix(path, "/tmp/"):
err = s.tmpStore.Delete(pathBase(path)) err = s.tmpStore.Delete(pathBase(path))
case strings.HasPrefix(path, "/tr/"): case strings.HasPrefix(path, "/tr/"):
@ -1174,7 +1228,7 @@ func (s *Server) handleWrite(path, input string) error {
// Tool file writes go to the tool store. // Tool file writes go to the tool store.
if strings.HasPrefix(path, "/t/") { if strings.HasPrefix(path, "/t/") {
return storeWrite(s.toolStore, pathBase(path), []byte(input)) return storeWrite(s.toolStore, strings.TrimPrefix(path, "/t/"), []byte(input))
} }
// Tmp file writes go to the tmp store. // Tmp file writes go to the tmp store.
@ -1274,7 +1328,13 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
if e.Name() == "idx" { if e.Name() == "idx" {
mode = 0444 mode = 0444
} }
dirs = append(dirs, makeDir(e.Name(), "/t/"+e.Name(), false, mode)) dirs = append(dirs, makeDir(e.Name(), "/t/"+e.Name(), e.IsDir(), mode|plan9.Perm(boolToDir(e.IsDir()))))
}
} else if strings.HasPrefix(path, "/t/") {
rel := strings.TrimPrefix(path, "/t/")
entries, _ := s.toolStore.ListDir(rel)
for _, e := range entries {
dirs = append(dirs, makeDir(e.Name(), path+"/"+e.Name(), e.IsDir(), 0777|plan9.Perm(boolToDir(e.IsDir()))))
} }
} else if path == "/tmp" { } else if path == "/tmp" {
entries, _ := s.tmpStore.List() entries, _ := s.tmpStore.List()
@ -1380,7 +1440,7 @@ func (s *Server) makeStat(path string) plan9.Dir {
var mode plan9.Perm var mode plan9.Perm
if isDir { if isDir {
qid.Type = QTDir qid.Type = QTDir
if path == "/t" { if path == "/t" || strings.HasPrefix(path, "/t/") {
mode = plan9.DMDIR | 0777 mode = plan9.DMDIR | 0777
} else if path == "/a" || path == "/m" || path == "/tmp" || path == "/u" { } else if path == "/a" || path == "/m" || path == "/tmp" || path == "/u" {
mode = plan9.DMDIR | 0755 mode = plan9.DMDIR | 0755
@ -1488,7 +1548,7 @@ func (s *Server) makeStat(path string) plan9.Dir {
dir.Length = uint64(len(content)) dir.Length = uint64(len(content))
} }
case strings.HasPrefix(path, "/t/"): case strings.HasPrefix(path, "/t/"):
if content, err := storeRead(s.toolStore, base); err == nil { if content, err := storeRead(s.toolStore, strings.TrimPrefix(path, "/t/")); err == nil {
dir.Length = uint64(len(content)) dir.Length = uint64(len(content))
} }
case strings.HasPrefix(path, "/u/"): case strings.HasPrefix(path, "/u/"):

View File

@ -78,7 +78,6 @@ func (s *storeConfig) Rename(old, new string) error { return s.RenameF
// NewFlatDir returns a Store backed by a directory on the local filesystem. // NewFlatDir returns a Store backed by a directory on the local filesystem.
func NewFlatDir(dir string, perm os.FileMode) Store { func NewFlatDir(dir string, perm os.FileMode) Store {
join := func(name string) string { return filepath.Join(dir, name) } join := func(name string) string { return filepath.Join(dir, name) }
ensureDir := func() error { return os.MkdirAll(dir, 0755) }
notBlocking := func(context.Context, string) ([]byte, error) { notBlocking := func(context.Context, string) ([]byte, error) {
return nil, fmt.Errorf("blocking read not supported") return nil, fmt.Errorf("blocking read not supported")
} }
@ -92,7 +91,7 @@ func NewFlatDir(dir string, perm os.FileMode) Store {
StatFn: func() (os.FileInfo, error) { return os.Stat(path) }, StatFn: func() (os.FileInfo, error) { return os.Stat(path) },
ReadFn: func() ([]byte, error) { return os.ReadFile(path) }, ReadFn: func() ([]byte, error) { return os.ReadFile(path) },
WriteFn: func(data []byte) error { WriteFn: func(data []byte) error {
if err := ensureDir(); err != nil { if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err return err
} }
return os.WriteFile(path, data, perm) return os.WriteFile(path, data, perm)
@ -101,10 +100,11 @@ func NewFlatDir(dir string, perm os.FileMode) Store {
}, nil }, nil
}, },
CreateFn: func(name string) error { CreateFn: func(name string) error {
if err := ensureDir(); err != nil { path := join(name)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err return err
} }
return os.WriteFile(join(name), nil, perm) return os.WriteFile(path, nil, perm)
}, },
DeleteFn: func(name string) error { return os.Remove(join(name)) }, DeleteFn: func(name string) error { return os.Remove(join(name)) },
RenameFn: func(old, new string) error { return os.Rename(join(old), join(new)) }, RenameFn: func(old, new string) error { return os.Rename(join(old), join(new)) },