admin 8dc496b626
Some checks failed
CI / test (push) Has been cancelled
Release / release (push) Failing after 4m36s
first commit
2026-03-08 15:40:34 +07:00

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
}