package config import ( "fmt" "os" "path/filepath" "strconv" "gopkg.in/yaml.v3" ) type Config struct { Ollama OllamaConfig `yaml:"ollama"` Model ModelConfig `yaml:"model,omitempty"` Agents AgentsConfig `yaml:"agents,omitempty"` Servers []ServerConfig `yaml:"servers,omitempty"` SkillsDir string `yaml:"skills_dir,omitempty"` ICE ICEConfig `yaml:"ice,omitempty"` AgentProfile string `yaml:"agent_profile,omitempty"` Tools ToolsConfig `yaml:"tools,omitempty"` } type AgentsConfig struct { Dir string `yaml:"dir,omitempty"` AutoLoad bool `yaml:"auto_load"` } type ToolsConfig struct { Timeout string `yaml:"timeout,omitempty"` // e.g., "30s", "2m" MaxGrepResults int `yaml:"max_grep_results,omitempty"` MaxIterations int `yaml:"max_iterations,omitempty"` } type ICEConfig struct { Enabled bool `yaml:"enabled"` EmbedModel string `yaml:"embed_model,omitempty"` StorePath string `yaml:"store_path,omitempty"` } type OllamaConfig struct { Model string `yaml:"model"` BaseURL string `yaml:"base_url"` NumCtx int `yaml:"num_ctx"` } type ServerConfig struct { Name string `yaml:"name"` Command string `yaml:"command,omitempty"` Args []string `yaml:"args,omitempty"` Env []string `yaml:"env,omitempty"` Transport string `yaml:"transport,omitempty"` URL string `yaml:"url,omitempty"` } func defaults() Config { modelCfg := DefaultModelConfig() return Config{ Ollama: OllamaConfig{ Model: "qwen3.5:2b", BaseURL: "http://localhost:11434", NumCtx: 262144, }, Model: modelCfg, Agents: AgentsConfig{ Dir: "", AutoLoad: true, }, Tools: ToolsConfig{ Timeout: "30s", MaxGrepResults: 500, MaxIterations: 10, }, } } func Load() (*Config, error) { cfg := defaults() localPath := findConfigFile() if localPath != "" { data, err := os.ReadFile(localPath) if err != nil { return nil, fmt.Errorf("read config %s: %w", localPath, err) } if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parse config %s: %w", localPath, err) } } agentsDir := cfg.Agents.Dir if agentsDir == "" { agentsDir = FindAgentsDir() } var agentsData *AgentsDir if agentsDir != "" && cfg.Agents.AutoLoad { var err error agentsData, err = LoadAgentsDir(agentsDir) if err != nil { fmt.Fprintf(os.Stderr, "warning: failed to load .agents directory: %v\n", err) } else { if agentsData != nil { if cfg.Ollama.Model == "" { cfg.Ollama.Model = cfg.Model.DefaultModel } if len(cfg.Servers) == 0 && agentsData.HasMCP() { cfg.Servers = agentsData.GetMCPServers() } } } } applyEnvOverrides(&cfg) return &cfg, nil } func LoadWithAgentsDir() (*Config, *AgentsDir, error) { cfg, err := Load() if err != nil { return nil, nil, err } agentsDir := cfg.Agents.Dir if agentsDir == "" { agentsDir = FindAgentsDir() } var agents *AgentsDir if agentsDir != "" && cfg.Agents.AutoLoad { agents, _ = LoadAgentsDir(agentsDir) } return cfg, agents, nil } func findConfigFile() string { candidates := []string{ "ai-agent.yaml", "ai-agent.yml", "config.yaml", "config.yml", } if home, err := os.UserHomeDir(); err == nil { candidates = append(candidates, filepath.Join(home, ".config", "ai-agent", "config.yaml"), filepath.Join(home, ".config", "ai-agent", "config.yml"), ) } for _, path := range candidates { if _, err := os.Stat(path); err == nil { return path } } return "" } func applyEnvOverrides(cfg *Config) { if v := os.Getenv("OLLAMA_HOST"); v != "" { cfg.Ollama.BaseURL = v } if v := os.Getenv("LOCAL_AGENT_MODEL"); v != "" { cfg.Ollama.Model = v } if v := os.Getenv("LOCAL_AGENT_AGENTS_DIR"); v != "" { cfg.Agents.Dir = v } if v := os.Getenv("LOCAL_AGENT_TOOLS_TIMEOUT"); v != "" { cfg.Tools.Timeout = v } if v := os.Getenv("LOCAL_AGENT_TOOLS_MAX_GREP"); v != "" { cfg.Tools.MaxGrepResults = parseEnvInt(v, cfg.Tools.MaxGrepResults) } if v := os.Getenv("LOCAL_AGENT_TOOLS_MAX_ITER"); v != "" { cfg.Tools.MaxIterations = parseEnvInt(v, cfg.Tools.MaxIterations) } if v := os.Getenv("LOCAL_AGENT_ICE_EMBED_MODEL"); v != "" { cfg.ICE.EmbedModel = v } } func parseEnvInt(v string, defaultVal int) int { if i, err := strconv.Atoi(v); err == nil { return i } return defaultVal }