From e031d2ba901b43854fad201ef20a79f993286afd Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Tue, 21 Apr 2026 13:52:12 +0200 Subject: [PATCH] p9: add configstore, move skills filesystem ops from core - configstore.go: loadAgentConfig helper for agent config loading - skillstore.go: absorbed List/Read/Dirs from skills package - Removed unused config imports from batchstore/server --- internal/p9/batchstore.go | 4 +-- internal/p9/configstore.go | 21 +++++++++++ internal/p9/server.go | 4 +-- internal/p9/skillstore.go | 71 +++++++++++++++++++++++++++++++++----- 4 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 internal/p9/configstore.go diff --git a/internal/p9/batchstore.go b/internal/p9/batchstore.go index 2a0a93f..5830024 100644 --- a/internal/p9/batchstore.go +++ b/internal/p9/batchstore.go @@ -10,7 +10,6 @@ import ( "ollie/pkg/agent" "ollie/pkg/backend" - "ollie/pkg/config" "ollie/pkg/tools" "ollie/pkg/tools/execute" ) @@ -282,8 +281,7 @@ func (s *BatchStore) executeJob(ctx context.Context, job *batchJob) (string, err be.SetModel(modelName) } - cfgPath := agent.AgentConfigPath(s.srv.agentsDir, agentName) - cfg, _ := config.Load(cfgPath) + cfg := loadAgentConfig(s.srv.agentsDir, agentName) newDisp := tools.NewDispatcherFunc(map[string]func() tools.Server{ "execute": execute.Decl(cwd), diff --git a/internal/p9/configstore.go b/internal/p9/configstore.go new file mode 100644 index 0000000..f78baa0 --- /dev/null +++ b/internal/p9/configstore.go @@ -0,0 +1,21 @@ +package p9 + +import ( + "os" + + "ollie/pkg/agent" + "ollie/pkg/config" +) + +// loadAgentConfig resolves and loads the config for a named agent. +// Returns nil (not an error) if the config file does not exist; +// BuildAgentEnv handles nil configs. +func loadAgentConfig(agentsDir, name string) *config.Config { + f, err := os.Open(agent.AgentConfigPath(agentsDir, name)) + if err != nil { + return nil + } + defer f.Close() + cfg, _ := config.Load(f) + return cfg +} diff --git a/internal/p9/server.go b/internal/p9/server.go index a7d2e15..2cf3637 100644 --- a/internal/p9/server.go +++ b/internal/p9/server.go @@ -46,7 +46,6 @@ import ( "ollie/pkg/agent" "ollie/pkg/backend" - "ollie/pkg/config" olog "ollie/pkg/log" "ollie/pkg/paths" "ollie/pkg/tools" @@ -1236,8 +1235,7 @@ func (s *Server) createSession(args []string) error { return fmt.Errorf("sessions dir: %w", err) } - cfgPath := agent.AgentConfigPath(s.agentsDir, agentName) - cfg, _ := config.Load(cfgPath) // nil cfg is handled by BuildAgentEnv + cfg := loadAgentConfig(s.agentsDir, agentName) newDisp := tools.NewDispatcherFunc(map[string]func() tools.Server{ "execute": execute.Decl(cwd), diff --git a/internal/p9/skillstore.go b/internal/p9/skillstore.go index f0182c3..e8d38cd 100644 --- a/internal/p9/skillstore.go +++ b/internal/p9/skillstore.go @@ -4,8 +4,10 @@ import ( "fmt" "os" "path/filepath" + "sort" "strings" + "ollie/pkg/paths" "ollie/pkg/skills" ) @@ -19,12 +21,63 @@ func NewSkillStore() *SkillStore { return &SkillStore{} } +// skillDirs returns the skill directories from OLLIE_SKILLS_PATH, +// falling back to the default skills directory. +func skillDirs() []string { + if env := os.Getenv("OLLIE_SKILLS_PATH"); env != "" { + return strings.Split(env, ":") + } + return []string{paths.CfgDir() + "/skills"} +} + +// listSkills scans all skill directories and returns deduplicated metadata. +// First occurrence by directory order wins on name collision. +func listSkills() []skills.Meta { + seen := make(map[string]bool) + var result []skills.Meta + for _, dir := range skillDirs() { + entries, err := os.ReadDir(dir) + if err != nil { + continue + } + for _, e := range entries { + if !e.IsDir() || seen[e.Name()] { + continue + } + skillDir := filepath.Join(dir, e.Name()) + f, err := os.Open(filepath.Join(skillDir, "SKILL.md")) + if err != nil { + continue + } + meta, err := skills.ParseFrontMatter(f, filepath.Base(skillDir), skillDir) + f.Close() + if err != nil { + continue + } + seen[meta.Name] = true + result = append(result, *meta) + } + } + sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name }) + return result +} + +// readSkill returns the SKILL.md content for the named skill. +func readSkill(name string) ([]byte, error) { + for _, m := range listSkills() { + if m.Name == name { + return os.ReadFile(filepath.Join(m.Dir, "SKILL.md")) + } + } + return nil, os.ErrNotExist +} + func (s *SkillStore) Stat(name string) (os.FileInfo, error) { if name == "idx" { return &syntheticFileInfo{name: "idx", mode: 0444}, nil } skillName := strings.TrimSuffix(name, ".md") - if _, err := skills.Read(skillName); err != nil { + if _, err := readSkill(skillName); err != nil { return nil, fmt.Errorf("%s: not found", name) } return &syntheticFileInfo{name: name, mode: 0666}, nil @@ -32,7 +85,7 @@ func (s *SkillStore) Stat(name string) (os.FileInfo, error) { func (s *SkillStore) List() ([]os.DirEntry, error) { result := []os.DirEntry{syntheticEntry("idx", 0444)} - for _, m := range skills.List() { + for _, m := range listSkills() { result = append(result, syntheticEntry(m.Name+".md", 0666)) } return result, nil @@ -43,20 +96,20 @@ func (s *SkillStore) Get(name string) ([]byte, error) { return s.index() } skillName := strings.TrimSuffix(name, ".md") - return skills.Read(skillName) + return readSkill(skillName) } func (s *SkillStore) Put(name string, data []byte) error { skillName := strings.TrimSuffix(name, ".md") dir := "" - for _, m := range skills.List() { + for _, m := range listSkills() { if m.Name == skillName { dir = m.Dir break } } if dir == "" { - dir = filepath.Join(skills.Dirs()[0], skillName) + dir = filepath.Join(skillDirs()[0], skillName) } if err := os.MkdirAll(dir, 0755); err != nil { return err @@ -66,7 +119,7 @@ func (s *SkillStore) Put(name string, data []byte) error { func (s *SkillStore) Delete(name string) error { skillName := strings.TrimSuffix(name, ".md") - for _, m := range skills.List() { + for _, m := range listSkills() { if m.Name == skillName { return os.RemoveAll(m.Dir) } @@ -77,7 +130,7 @@ func (s *SkillStore) Delete(name string) error { func (s *SkillStore) Rename(oldName, newName string) error { oldSkill := strings.TrimSuffix(oldName, ".md") newSkill := strings.TrimSuffix(newName, ".md") - for _, m := range skills.List() { + for _, m := range listSkills() { if m.Name == oldSkill { newDir := filepath.Join(filepath.Dir(m.Dir), newSkill) return os.Rename(m.Dir, newDir) @@ -88,7 +141,7 @@ func (s *SkillStore) Rename(oldName, newName string) error { func (s *SkillStore) Create(name string) error { skillName := strings.TrimSuffix(name, ".md") - dir := filepath.Join(skills.Dirs()[0], skillName) + dir := filepath.Join(skillDirs()[0], skillName) if err := os.MkdirAll(dir, 0755); err != nil { return err } @@ -97,7 +150,7 @@ func (s *SkillStore) Create(name string) error { func (s *SkillStore) index() ([]byte, error) { var sb strings.Builder - for _, m := range skills.List() { + for _, m := range listSkills() { fmt.Fprintf(&sb, "## %s\n", m.Name) fmt.Fprintf(&sb, "description: %s\n", m.Description) sb.WriteString("\n")