1// Package config manages application configuration from various sources.
2package config
3
4import (
5 "fmt"
6 "log/slog"
7 "os"
8 "strings"
9
10 "github.com/kujtimiihoxha/termai/internal/llm/models"
11 "github.com/kujtimiihoxha/termai/internal/logging"
12 "github.com/spf13/viper"
13)
14
15// MCPType defines the type of MCP (Model Control Protocol) server.
16type MCPType string
17
18// Supported MCP types
19const (
20 MCPStdio MCPType = "stdio"
21 MCPSse MCPType = "sse"
22)
23
24// MCPServer defines the configuration for a Model Control Protocol server.
25type MCPServer struct {
26 Command string `json:"command"`
27 Env []string `json:"env"`
28 Args []string `json:"args"`
29 Type MCPType `json:"type"`
30 URL string `json:"url"`
31 Headers map[string]string `json:"headers"`
32}
33
34// Model defines configuration for different LLM models and their token limits.
35type Model struct {
36 Coder models.ModelID `json:"coder"`
37 CoderMaxTokens int64 `json:"coderMaxTokens"`
38 Task models.ModelID `json:"task"`
39 TaskMaxTokens int64 `json:"taskMaxTokens"`
40}
41
42// Provider defines configuration for an LLM provider.
43type Provider struct {
44 APIKey string `json:"apiKey"`
45 Disabled bool `json:"disabled"`
46}
47
48// Data defines storage configuration.
49type Data struct {
50 Directory string `json:"directory"`
51}
52
53// LSPConfig defines configuration for Language Server Protocol integration.
54type LSPConfig struct {
55 Disabled bool `json:"enabled"`
56 Command string `json:"command"`
57 Args []string `json:"args"`
58 Options any `json:"options"`
59}
60
61// Config is the main configuration structure for the application.
62type Config struct {
63 Data Data `json:"data"`
64 WorkingDir string `json:"wd,omitempty"`
65 MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
66 Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
67 LSP map[string]LSPConfig `json:"lsp,omitempty"`
68 Model Model `json:"model"`
69 Debug bool `json:"debug,omitempty"`
70}
71
72// Application constants
73const (
74 defaultDataDirectory = ".opencode"
75 defaultLogLevel = "info"
76 defaultMaxTokens = int64(5000)
77 appName = "opencode"
78)
79
80// Global configuration instance
81var cfg *Config
82
83// Load initializes the configuration from environment variables and config files.
84// If debug is true, debug mode is enabled and log level is set to debug.
85// It returns an error if configuration loading fails.
86func Load(workingDir string, debug bool) (*Config, error) {
87 if cfg != nil {
88 return cfg, nil
89 }
90
91 cfg = &Config{
92 WorkingDir: workingDir,
93 MCPServers: make(map[string]MCPServer),
94 Providers: make(map[models.ModelProvider]Provider),
95 LSP: make(map[string]LSPConfig),
96 }
97
98 configureViper()
99 setDefaults(debug)
100 setProviderDefaults()
101
102 // Read global config
103 if err := readConfig(viper.ReadInConfig()); err != nil {
104 return cfg, err
105 }
106
107 // Load and merge local config
108 mergeLocalConfig(workingDir)
109
110 // Apply configuration to the struct
111 if err := viper.Unmarshal(cfg); err != nil {
112 return cfg, fmt.Errorf("failed to unmarshal config: %w", err)
113 }
114
115 applyDefaultValues()
116
117 defaultLevel := slog.LevelInfo
118 if cfg.Debug {
119 defaultLevel = slog.LevelDebug
120 }
121 // Configure logger
122 logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
123 Level: defaultLevel,
124 }))
125 slog.SetDefault(logger)
126 return cfg, nil
127}
128
129// configureViper sets up viper's configuration paths and environment variables.
130func configureViper() {
131 viper.SetConfigName(fmt.Sprintf(".%s", appName))
132 viper.SetConfigType("json")
133 viper.AddConfigPath("$HOME")
134 viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
135 viper.SetEnvPrefix(strings.ToUpper(appName))
136 viper.AutomaticEnv()
137}
138
139// setDefaults configures default values for configuration options.
140func setDefaults(debug bool) {
141 viper.SetDefault("data.directory", defaultDataDirectory)
142
143 if debug {
144 viper.SetDefault("debug", true)
145 viper.Set("log.level", "debug")
146 } else {
147 viper.SetDefault("debug", false)
148 viper.SetDefault("log.level", defaultLogLevel)
149 }
150}
151
152// setProviderDefaults configures LLM provider defaults based on environment variables.
153// the default model priority is:
154// 1. Anthropic
155// 2. OpenAI
156// 3. Google Gemini
157// 4. AWS Bedrock
158func setProviderDefaults() {
159 // Groq configuration
160 if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
161 viper.SetDefault("providers.groq.apiKey", apiKey)
162 viper.SetDefault("model.coder", models.QWENQwq)
163 viper.SetDefault("model.coderMaxTokens", defaultMaxTokens)
164 viper.SetDefault("model.task", models.QWENQwq)
165 viper.SetDefault("model.taskMaxTokens", defaultMaxTokens)
166 }
167
168 // Google Gemini configuration
169 if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
170 viper.SetDefault("providers.gemini.apiKey", apiKey)
171 viper.SetDefault("model.coder", models.GRMINI20Flash)
172 viper.SetDefault("model.coderMaxTokens", defaultMaxTokens)
173 viper.SetDefault("model.task", models.GRMINI20Flash)
174 viper.SetDefault("model.taskMaxTokens", defaultMaxTokens)
175 }
176
177 // OpenAI configuration
178 if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
179 viper.SetDefault("providers.openai.apiKey", apiKey)
180 viper.SetDefault("model.coder", models.GPT4o)
181 viper.SetDefault("model.coderMaxTokens", defaultMaxTokens)
182 viper.SetDefault("model.task", models.GPT4o)
183 viper.SetDefault("model.taskMaxTokens", defaultMaxTokens)
184 }
185
186 // Anthropic configuration
187 if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
188 viper.SetDefault("providers.anthropic.apiKey", apiKey)
189 viper.SetDefault("model.coder", models.Claude37Sonnet)
190 viper.SetDefault("model.coderMaxTokens", defaultMaxTokens)
191 viper.SetDefault("model.task", models.Claude37Sonnet)
192 viper.SetDefault("model.taskMaxTokens", defaultMaxTokens)
193 }
194
195 if hasAWSCredentials() {
196 viper.SetDefault("model.coder", models.BedrockClaude37Sonnet)
197 viper.SetDefault("model.coderMaxTokens", defaultMaxTokens)
198 viper.SetDefault("model.task", models.BedrockClaude37Sonnet)
199 viper.SetDefault("model.taskMaxTokens", defaultMaxTokens)
200 }
201}
202
203// hasAWSCredentials checks if AWS credentials are available in the environment.
204func hasAWSCredentials() bool {
205 // Check for explicit AWS credentials
206 if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
207 return true
208 }
209
210 // Check for AWS profile
211 if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
212 return true
213 }
214
215 // Check for AWS region
216 if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" {
217 return true
218 }
219
220 // Check if running on EC2 with instance profile
221 if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
222 os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
223 return true
224 }
225
226 return false
227}
228
229// readConfig handles the result of reading a configuration file.
230func readConfig(err error) error {
231 if err == nil {
232 return nil
233 }
234
235 // It's okay if the config file doesn't exist
236 if _, ok := err.(viper.ConfigFileNotFoundError); ok {
237 return nil
238 }
239
240 return fmt.Errorf("failed to read config: %w", err)
241}
242
243// mergeLocalConfig loads and merges configuration from the local directory.
244func mergeLocalConfig(workingDir string) {
245 local := viper.New()
246 local.SetConfigName(fmt.Sprintf(".%s", appName))
247 local.SetConfigType("json")
248 local.AddConfigPath(workingDir)
249
250 // Merge local config if it exists
251 if err := local.ReadInConfig(); err == nil {
252 viper.MergeConfigMap(local.AllSettings())
253 }
254}
255
256// applyDefaultValues sets default values for configuration fields that need processing.
257func applyDefaultValues() {
258 // Set default MCP type if not specified
259 for k, v := range cfg.MCPServers {
260 if v.Type == "" {
261 v.Type = MCPStdio
262 cfg.MCPServers[k] = v
263 }
264 }
265}
266
267// Get returns the current configuration.
268// It's safe to call this function multiple times.
269func Get() *Config {
270 return cfg
271}
272
273// WorkingDirectory returns the current working directory from the configuration.
274func WorkingDirectory() string {
275 return viper.GetString("wd")
276}