205 lines
5.4 KiB
Go
205 lines
5.4 KiB
Go
package library
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Setup is which agents get what from the library, and the instructions,
|
|
// as a profile keeps them to switch back to. The servers and the skills
|
|
// themselves stay in the library: a setup only names them.
|
|
type Setup struct {
|
|
Instructions SetupInstructions `json:"instructions"`
|
|
MCP map[string][]string `json:"mcp"` // server → the agents that get it
|
|
Skills map[string][]string `json:"skills"` // skill → the agents that get it
|
|
}
|
|
|
|
// SetupInstructions is the shared text, the agents it is written to, and
|
|
// what each agent gets besides.
|
|
type SetupInstructions struct {
|
|
Set string `json:"set,omitempty"` // the set it is the text of
|
|
Shared string `json:"shared,omitempty"`
|
|
Agents []string `json:"agents"`
|
|
Extra map[string]string `json:"extra,omitempty"`
|
|
}
|
|
|
|
// Empty reports whether the library had nothing in it when s was taken.
|
|
func (s *Setup) Empty() bool {
|
|
i := s.Instructions
|
|
return len(s.MCP) == 0 && len(s.Skills) == 0 && i.Shared == "" && len(i.Agents) == 0 && len(i.Extra) == 0
|
|
}
|
|
|
|
// Summary is what a setup gives out, in a few words: the servers and
|
|
// skills given to at least one agent, and whether any agent gets the
|
|
// instructions.
|
|
func (s *Setup) Summary() string {
|
|
count := func(m map[string][]string, one, many string) string {
|
|
n := 0
|
|
for _, agents := range m {
|
|
if len(agents) > 0 {
|
|
n++
|
|
}
|
|
}
|
|
if n == 0 {
|
|
return ""
|
|
}
|
|
if n == 1 {
|
|
return "1 " + one
|
|
}
|
|
return fmt.Sprintf("%d %s", n, many)
|
|
}
|
|
var parts []string
|
|
for _, p := range []string{count(s.MCP, "server", "servers"), count(s.Skills, "skill", "skills")} {
|
|
if p != "" {
|
|
parts = append(parts, p)
|
|
}
|
|
}
|
|
if s.GivesInstructions() {
|
|
parts = append(parts, "instructions")
|
|
}
|
|
if len(parts) == 0 {
|
|
return "library: nothing on"
|
|
}
|
|
return "library: " + strings.Join(parts, ", ")
|
|
}
|
|
|
|
// GivesInstructions reports whether any agent gets instructions from it.
|
|
func (s *Setup) GivesInstructions() bool {
|
|
i := s.Instructions
|
|
if i.Shared != "" && len(i.Agents) > 0 {
|
|
return true
|
|
}
|
|
for _, id := range i.Agents {
|
|
if i.Extra[id] != "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// On counts the servers and the skills a setup gives to at least one agent.
|
|
func (s *Setup) On() (servers, skills int) {
|
|
for _, a := range s.MCP {
|
|
if len(a) > 0 {
|
|
servers++
|
|
}
|
|
}
|
|
for _, a := range s.Skills {
|
|
if len(a) > 0 {
|
|
skills++
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func extras() map[string]string {
|
|
out := map[string]string{}
|
|
es, _ := os.ReadDir(filepath.Dir(extraPath("x")))
|
|
for _, e := range es {
|
|
id, ok := strings.CutSuffix(e.Name(), ".md")
|
|
id = agentOfFile(id)
|
|
if !ok || e.IsDir() || checkAgent(id) != nil {
|
|
continue
|
|
}
|
|
if text := readText(extraPath(id)); text != "" {
|
|
out[id] = text
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Snapshot is the library's setup as it is now.
|
|
func Snapshot() (*Setup, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
l, err := load()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s := &Setup{
|
|
Instructions: SetupInstructions{Set: l.active(), Shared: readText(l.sharedPath()), Agents: slices.Clone(l.Instructions.Agents), Extra: extras()},
|
|
MCP: map[string][]string{},
|
|
Skills: map[string][]string{},
|
|
}
|
|
if s.Instructions.Agents == nil {
|
|
s.Instructions.Agents = []string{}
|
|
}
|
|
if len(s.Instructions.Extra) == 0 {
|
|
s.Instructions.Extra = nil
|
|
}
|
|
for _, x := range l.MCP {
|
|
s.MCP[x.Name] = append([]string{}, x.Agents...)
|
|
}
|
|
for _, x := range l.Skills {
|
|
s.Skills[x.Name] = append([]string{}, x.Agents...)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// Restore gives the servers and skills to the agents s names and puts the
|
|
// instructions back as they were, then writes the library into the agents.
|
|
// A server or skill s names that the library no longer has is left out and
|
|
// listed in Result.Missing; one added to the library since s was taken is
|
|
// left with the agents it has, as s knows nothing of it. The library's own
|
|
// instructions files that change are kept aside with the agents' files,
|
|
// in the same backup.
|
|
func Restore(s *Setup) (*Result, error) {
|
|
var missing []string
|
|
res, err := change(func(l *Library) error {
|
|
missing = nil
|
|
b := newBackups()
|
|
l.kept = b
|
|
// the text as it is, then as s has it: a file that changes is kept first
|
|
// the set s was taken on, if it is still kept, gets its text back
|
|
if l.hasSet(s.Instructions.Set) {
|
|
l.Instructions.Active = s.Instructions.Set
|
|
}
|
|
texts := map[string]string{l.sharedPath(): s.Instructions.Shared}
|
|
for id := range extras() {
|
|
texts[extraPath(id)] = ""
|
|
}
|
|
for id, text := range s.Instructions.Extra {
|
|
if err := checkAgent(id); err != nil {
|
|
return err
|
|
}
|
|
texts[extraPath(id)] = text
|
|
}
|
|
for p, text := range texts {
|
|
if readText(p) == strings.TrimSpace(text) {
|
|
continue
|
|
}
|
|
if err := b.keep("library", p); err != nil {
|
|
return err
|
|
}
|
|
if err := writeText(p, text); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
l.Instructions.Agents = slices.Sorted(slices.Values(s.Instructions.Agents))
|
|
for name, agents := range s.MCP {
|
|
if x := l.server(name); x != nil {
|
|
x.Agents = slices.Sorted(slices.Values(agents))
|
|
} else {
|
|
missing = append(missing, "mcp:"+name)
|
|
}
|
|
}
|
|
for name, agents := range s.Skills {
|
|
if x := l.skill(name); x != nil {
|
|
x.Agents = slices.Sorted(slices.Values(agents))
|
|
} else {
|
|
missing = append(missing, "skill:"+name)
|
|
}
|
|
}
|
|
sort.Strings(missing)
|
|
return nil
|
|
})
|
|
if res != nil {
|
|
res.Missing = missing
|
|
}
|
|
return res, err
|
|
}
|