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/opencode/internal/llm/models"
11 "github.com/kujtimiihoxha/opencode/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
34type AgentName string
35
36const (
37 AgentCoder AgentName = "coder"
38 AgentTask AgentName = "task"
39 AgentTitle AgentName = "title"
40)
41
42// Agent defines configuration for different LLM models and their token limits.
43type Agent struct {
44 Model models.ModelID `json:"model"`
45 MaxTokens int64 `json:"maxTokens"`
46}
47
48// Provider defines configuration for an LLM provider.
49type Provider struct {
50 APIKey string `json:"apiKey"`
51 Disabled bool `json:"disabled"`
52}
53
54// Data defines storage configuration.
55type Data struct {
56 Directory string `json:"directory"`
57}
58
59// LSPConfig defines configuration for Language Server Protocol integration.
60type LSPConfig struct {
61 Disabled bool `json:"enabled"`
62 Command string `json:"command"`
63 Args []string `json:"args"`
64 Options any `json:"options"`
65}
66
67// Config is the main configuration structure for the application.
68type Config struct {
69 Data Data `json:"data"`
70 WorkingDir string `json:"wd,omitempty"`
71 MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
72 Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
73 LSP map[string]LSPConfig `json:"lsp,omitempty"`
74 Agents map[AgentName]Agent `json:"agents"`
75 Debug bool `json:"debug,omitempty"`
76 DebugLSP bool `json:"debugLSP,omitempty"`
77}
78
79// Application constants
80const (
81 defaultDataDirectory = ".opencode"
82 defaultLogLevel = "info"
83 defaultMaxTokens = int64(5000)
84 appName = "opencode"
85)
86
87// Global configuration instance
88var cfg *Config
89
90// Load initializes the configuration from environment variables and config files.
91// If debug is true, debug mode is enabled and log level is set to debug.
92// It returns an error if configuration loading fails.
93func Load(workingDir string, debug bool) (*Config, error) {
94 if cfg != nil {
95 return cfg, nil
96 }
97
98 cfg = &Config{
99 WorkingDir: workingDir,
100 MCPServers: make(map[string]MCPServer),
101 Providers: make(map[models.ModelProvider]Provider),
102 LSP: make(map[string]LSPConfig),
103 }
104
105 configureViper()
106 setDefaults(debug)
107 setProviderDefaults()
108
109 // Read global config
110 if err := readConfig(viper.ReadInConfig()); err != nil {
111 return cfg, err
112 }
113
114 // Load and merge local config
115 mergeLocalConfig(workingDir)
116
117 // Apply configuration to the struct
118 if err := viper.Unmarshal(cfg); err != nil {
119 return cfg, fmt.Errorf("failed to unmarshal config: %w", err)
120 }
121
122 applyDefaultValues()
123
124 defaultLevel := slog.LevelInfo
125 if cfg.Debug {
126 defaultLevel = slog.LevelDebug
127 }
128 // if we are in debug mode make the writer a file
129 if cfg.Debug {
130 loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log")
131
132 // if file does not exist create it
133 if _, err := os.Stat(loggingFile); os.IsNotExist(err) {
134 if err := os.MkdirAll(cfg.Data.Directory, 0o755); err != nil {
135 return cfg, fmt.Errorf("failed to create directory: %w", err)
136 }
137 if _, err := os.Create(loggingFile); err != nil {
138 return cfg, fmt.Errorf("failed to create log file: %w", err)
139 }
140 }
141
142 sloggingFileWriter, err := os.OpenFile(loggingFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
143 if err != nil {
144 return cfg, fmt.Errorf("failed to open log file: %w", err)
145 }
146 // Configure logger
147 logger := slog.New(slog.NewTextHandler(sloggingFileWriter, &slog.HandlerOptions{
148 Level: defaultLevel,
149 }))
150 slog.SetDefault(logger)
151 } else {
152 // Configure logger
153 logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
154 Level: defaultLevel,
155 }))
156 slog.SetDefault(logger)
157 }
158
159 if cfg.Agents == nil {
160 cfg.Agents = make(map[AgentName]Agent)
161 }
162
163 // Override the max tokens for title agent
164 cfg.Agents[AgentTitle] = Agent{
165 Model: cfg.Agents[AgentTitle].Model,
166 MaxTokens: 80,
167 }
168 return cfg, nil
169}
170
171// configureViper sets up viper's configuration paths and environment variables.
172func configureViper() {
173 viper.SetConfigName(fmt.Sprintf(".%s", appName))
174 viper.SetConfigType("json")
175 viper.AddConfigPath("$HOME")
176 viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
177 viper.SetEnvPrefix(strings.ToUpper(appName))
178 viper.AutomaticEnv()
179}
180
181// setDefaults configures default values for configuration options.
182func setDefaults(debug bool) {
183 viper.SetDefault("data.directory", defaultDataDirectory)
184
185 if debug {
186 viper.SetDefault("debug", true)
187 viper.Set("log.level", "debug")
188 } else {
189 viper.SetDefault("debug", false)
190 viper.SetDefault("log.level", defaultLogLevel)
191 }
192}
193
194// setProviderDefaults configures LLM provider defaults based on environment variables.
195// the default model priority is:
196// 1. Anthropic
197// 2. OpenAI
198// 3. Google Gemini
199// 4. AWS Bedrock
200func setProviderDefaults() {
201 // Groq configuration
202 if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
203 viper.SetDefault("providers.groq.apiKey", apiKey)
204 viper.SetDefault("agents.coder.model", models.QWENQwq)
205 viper.SetDefault("agents.coder.maxTokens", defaultMaxTokens)
206 viper.SetDefault("agents.task.model", models.QWENQwq)
207 viper.SetDefault("agents.task.maxTokens", defaultMaxTokens)
208 viper.SetDefault("agents.title.model", models.QWENQwq)
209 }
210
211 // Google Gemini configuration
212 if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
213 viper.SetDefault("providers.gemini.apiKey", apiKey)
214 viper.SetDefault("agents.coder.model", models.GRMINI20Flash)
215 viper.SetDefault("agents.coder.maxTokens", defaultMaxTokens)
216 viper.SetDefault("agents.task.model", models.GRMINI20Flash)
217 viper.SetDefault("agents.task.maxTokens", defaultMaxTokens)
218 viper.SetDefault("agents.title.model", models.GRMINI20Flash)
219 }
220
221 // OpenAI configuration
222 if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
223 viper.SetDefault("providers.openai.apiKey", apiKey)
224 viper.SetDefault("agents.coder.model", models.GPT4o)
225 viper.SetDefault("agents.coder.maxTokens", defaultMaxTokens)
226 viper.SetDefault("agents.task.model", models.GPT4o)
227 viper.SetDefault("agents.task.maxTokens", defaultMaxTokens)
228 viper.SetDefault("agents.title.model", models.GPT4o)
229
230 }
231
232 // Anthropic configuration
233 if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
234 viper.SetDefault("providers.anthropic.apiKey", apiKey)
235 viper.SetDefault("agents.coder.model", models.Claude37Sonnet)
236 viper.SetDefault("agents.coder.maxTokens", defaultMaxTokens)
237 viper.SetDefault("agents.task.model", models.Claude37Sonnet)
238 viper.SetDefault("agents.task.maxTokens", defaultMaxTokens)
239 viper.SetDefault("agents.title.model", models.Claude37Sonnet)
240 }
241
242 if hasAWSCredentials() {
243 viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
244 viper.SetDefault("agents.coder.maxTokens", defaultMaxTokens)
245 viper.SetDefault("agents.task.model", models.BedrockClaude37Sonnet)
246 viper.SetDefault("agents.task.maxTokens", defaultMaxTokens)
247 viper.SetDefault("agents.title.model", models.BedrockClaude37Sonnet)
248 }
249}
250
251// hasAWSCredentials checks if AWS credentials are available in the environment.
252func hasAWSCredentials() bool {
253 // Check for explicit AWS credentials
254 if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
255 return true
256 }
257
258 // Check for AWS profile
259 if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
260 return true
261 }
262
263 // Check for AWS region
264 if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" {
265 return true
266 }
267
268 // Check if running on EC2 with instance profile
269 if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
270 os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
271 return true
272 }
273
274 return false
275}
276
277// readConfig handles the result of reading a configuration file.
278func readConfig(err error) error {
279 if err == nil {
280 return nil
281 }
282
283 // It's okay if the config file doesn't exist
284 if _, ok := err.(viper.ConfigFileNotFoundError); ok {
285 return nil
286 }
287
288 return fmt.Errorf("failed to read config: %w", err)
289}
290
291// mergeLocalConfig loads and merges configuration from the local directory.
292func mergeLocalConfig(workingDir string) {
293 local := viper.New()
294 local.SetConfigName(fmt.Sprintf(".%s", appName))
295 local.SetConfigType("json")
296 local.AddConfigPath(workingDir)
297
298 // Merge local config if it exists
299 if err := local.ReadInConfig(); err == nil {
300 viper.MergeConfigMap(local.AllSettings())
301 }
302}
303
304// applyDefaultValues sets default values for configuration fields that need processing.
305func applyDefaultValues() {
306 // Set default MCP type if not specified
307 for k, v := range cfg.MCPServers {
308 if v.Type == "" {
309 v.Type = MCPStdio
310 cfg.MCPServers[k] = v
311 }
312 }
313}
314
315// Get returns the current configuration.
316// It's safe to call this function multiple times.
317func Get() *Config {
318 return cfg
319}
320
321// WorkingDirectory returns the current working directory from the configuration.
322func WorkingDirectory() string {
323 if cfg == nil {
324 panic("config not loaded")
325 }
326 return cfg.WorkingDir
327}