367 lines
8.9 KiB
Go
367 lines
8.9 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type AgentsDir struct {
|
|
Path string
|
|
Agents map[string]AgentProfile
|
|
MCPServers []ServerConfig
|
|
GlobalInstructions string
|
|
Skills []SkillDef
|
|
}
|
|
|
|
type AgentProfile struct {
|
|
Name string `yaml:"name" json:"name"`
|
|
Description string `yaml:"description" json:"description"`
|
|
Model string `yaml:"model" json:"model"`
|
|
Skills []string `yaml:"skills" json:"skills"`
|
|
MCPServers []string `yaml:"mcp_servers" json:"mcp_servers"`
|
|
SystemPrompt string `yaml:"system_prompt" json:"system_prompt"`
|
|
UseCases []string `yaml:"use_cases" json:"use_cases"`
|
|
}
|
|
|
|
type SkillDef struct {
|
|
Name string `yaml:"name" json:"name"`
|
|
Description string `yaml:"description" json:"description"`
|
|
Path string `yaml:"path" json:"path"`
|
|
}
|
|
|
|
type MCPConfig struct {
|
|
Servers []ServerConfig `json:"servers,omitempty"`
|
|
}
|
|
|
|
type ModelsConfig struct {
|
|
Models []Model `yaml:"models,omitempty"`
|
|
DefaultModel string `yaml:"default_model,omitempty"`
|
|
FallbackChain []string `yaml:"fallback_chain,omitempty"`
|
|
AutoSelect bool `yaml:"auto_select,omitempty"`
|
|
EmbedModel string `yaml:"embed_model,omitempty"`
|
|
}
|
|
|
|
func FindAgentsDir() string {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
candidates := []string{
|
|
filepath.Join(home, ".agents"),
|
|
filepath.Join(home, ".config", "agents"),
|
|
}
|
|
|
|
for _, dir := range candidates {
|
|
if _, err := os.Stat(dir); err == nil {
|
|
return dir
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func FindAgentsDirWithCreate() (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("get home dir: %w", err)
|
|
}
|
|
|
|
dirs := []string{
|
|
filepath.Join(home, ".agents"),
|
|
filepath.Join(home, ".config", "agents"),
|
|
}
|
|
|
|
for _, dir := range dirs {
|
|
if _, err := os.Stat(dir); err == nil {
|
|
return dir, nil
|
|
}
|
|
}
|
|
|
|
if err := os.MkdirAll(dirs[0], 0755); err != nil {
|
|
return "", fmt.Errorf("create agents dir: %w", err)
|
|
}
|
|
|
|
return dirs[0], nil
|
|
}
|
|
|
|
func LoadAgentsDir(path string) (*AgentsDir, error) {
|
|
if path == "" {
|
|
path = FindAgentsDir()
|
|
if path == "" {
|
|
return &AgentsDir{
|
|
Path: "",
|
|
Agents: make(map[string]AgentProfile),
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
dir := &AgentsDir{
|
|
Path: path,
|
|
Agents: make(map[string]AgentProfile),
|
|
}
|
|
|
|
if err := dir.loadAgents(path); err != nil {
|
|
return nil, fmt.Errorf("load agents: %w", err)
|
|
}
|
|
|
|
if err := dir.loadMCP(path); err != nil {
|
|
return nil, fmt.Errorf("load MCP: %w", err)
|
|
}
|
|
|
|
if err := dir.loadGlobalInstructions(path); err != nil {
|
|
return nil, fmt.Errorf("load instructions: %w", err)
|
|
}
|
|
|
|
if err := dir.loadSkills(path); err != nil {
|
|
return nil, fmt.Errorf("load skills: %w", err)
|
|
}
|
|
|
|
return dir, nil
|
|
}
|
|
|
|
func (d *AgentsDir) loadAgents(path string) error {
|
|
agentsDir := filepath.Join(path, "agents")
|
|
entries, err := os.ReadDir(agentsDir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
agentPath := filepath.Join(agentsDir, entry.Name(), "agent.yaml")
|
|
if _, err := os.Stat(agentPath); err != nil {
|
|
agentPath = filepath.Join(agentsDir, entry.Name(), "agent.md")
|
|
}
|
|
if _, err := os.Stat(agentPath); err != nil {
|
|
continue
|
|
}
|
|
|
|
data, err := os.ReadFile(agentPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
var profile AgentProfile
|
|
if err := yaml.Unmarshal(data, &profile); err != nil {
|
|
continue
|
|
}
|
|
|
|
if profile.Name == "" {
|
|
profile.Name = entry.Name()
|
|
}
|
|
|
|
d.Agents[profile.Name] = profile
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *AgentsDir) loadMCP(path string) error {
|
|
mcpPath := filepath.Join(path, "mcp.json")
|
|
data, err := os.ReadFile(mcpPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
var mcpCfg MCPConfig
|
|
if err := json.Unmarshal(data, &mcpCfg); err != nil {
|
|
return fmt.Errorf("parse mcp.json: %w", err)
|
|
}
|
|
|
|
d.MCPServers = mcpCfg.Servers
|
|
return nil
|
|
}
|
|
|
|
func (d *AgentsDir) loadGlobalInstructions(path string) error {
|
|
paths := []string{
|
|
filepath.Join(path, "agents.md"),
|
|
filepath.Join(path, "instructions.md"),
|
|
}
|
|
|
|
for _, p := range paths {
|
|
data, err := os.ReadFile(p)
|
|
if err == nil {
|
|
d.GlobalInstructions = string(data)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *AgentsDir) loadSkills(path string) error {
|
|
skillsDir := filepath.Join(path, "skills")
|
|
entries, err := os.ReadDir(skillsDir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
skillDir := filepath.Join(skillsDir, entry.Name())
|
|
|
|
// Try both SKILL.md and skill.md (case insensitive check)
|
|
skillPath := ""
|
|
for _, name := range []string{"SKILL.md", "skill.md"} {
|
|
path := filepath.Join(skillDir, name)
|
|
if info, err := os.Stat(path); err == nil && !info.IsDir() {
|
|
skillPath = path
|
|
break
|
|
}
|
|
}
|
|
|
|
if skillPath == "" {
|
|
continue
|
|
}
|
|
|
|
data, err := os.ReadFile(skillPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
d.Skills = append(d.Skills, SkillDef{
|
|
Name: entry.Name(),
|
|
Description: extractDescription(string(data)),
|
|
Path: skillPath,
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func extractDescription(content string) string {
|
|
for _, line := range splitLines(content) {
|
|
line = trimWhitespace(line)
|
|
if line == "" || startsWith(line, "#") {
|
|
continue
|
|
}
|
|
return line
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func splitLines(s string) []string {
|
|
var lines []string
|
|
start := 0
|
|
for i, r := range s {
|
|
if r == '\n' {
|
|
lines = append(lines, s[start:i])
|
|
start = i + 1
|
|
}
|
|
}
|
|
lines = append(lines, s[start:])
|
|
return lines
|
|
}
|
|
|
|
func trimWhitespace(s string) string {
|
|
start := 0
|
|
end := len(s)
|
|
for start < end && (s[start] == ' ' || s[start] == '\t') {
|
|
start++
|
|
}
|
|
for end > start && (s[end-1] == ' ' || s[end-1] == '\t') {
|
|
end--
|
|
}
|
|
return s[start:end]
|
|
}
|
|
|
|
func startsWith(s, prefix string) bool {
|
|
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
|
|
}
|
|
|
|
func (d *AgentsDir) GetAgent(name string) *AgentProfile {
|
|
if agent, ok := d.Agents[name]; ok {
|
|
return &agent
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *AgentsDir) ListAgents() []AgentProfile {
|
|
agents := make([]AgentProfile, 0, len(d.Agents))
|
|
for _, agent := range d.Agents {
|
|
agents = append(agents, agent)
|
|
}
|
|
return agents
|
|
}
|
|
|
|
func (d *AgentsDir) GetSkills() []SkillDef {
|
|
return d.Skills
|
|
}
|
|
|
|
func (d *AgentsDir) HasMCP() bool {
|
|
return len(d.MCPServers) > 0
|
|
}
|
|
|
|
func (d *AgentsDir) GetMCPServers() []ServerConfig {
|
|
return d.MCPServers
|
|
}
|
|
|
|
func (d *AgentsDir) GetGlobalInstructions() string {
|
|
return d.GlobalInstructions
|
|
}
|
|
|
|
func CreateDefaultAgentsDir() error {
|
|
dir, err := FindAgentsDirWithCreate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
subdirs := []string{"agents", "skills", "tasks", "memories"}
|
|
for _, sub := range subdirs {
|
|
path := filepath.Join(dir, sub)
|
|
if _, err := os.Stat(path); err != nil {
|
|
if err := os.MkdirAll(path, 0755); err != nil {
|
|
return fmt.Errorf("create %s: %w", sub, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
mcpPath := filepath.Join(dir, "mcp.json")
|
|
if _, err := os.Stat(mcpPath); err != nil {
|
|
defaultMCP := MCPConfig{
|
|
Servers: []ServerConfig{},
|
|
}
|
|
data, _ := json.MarshalIndent(defaultMCP, "", " ")
|
|
if err := os.WriteFile(mcpPath, data, 0644); err != nil {
|
|
return fmt.Errorf("write mcp.json: %w", err)
|
|
}
|
|
}
|
|
|
|
agentsPath := filepath.Join(dir, "agents.md")
|
|
if _, err := os.Stat(agentsPath); err != nil {
|
|
defaultContent := `# Global Agent Instructions
|
|
|
|
You are a helpful local AI coding assistant.
|
|
|
|
## Guidelines
|
|
- Be concise and direct
|
|
- Explain your reasoning
|
|
- Ask for clarification when needed
|
|
- Never fabricate information
|
|
`
|
|
if err := os.WriteFile(agentsPath, []byte(defaultContent), 0644); err != nil {
|
|
return fmt.Errorf("write agents.md: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|