cleanup config

Kujtim Hoxha created

Change summary

internal/config/config.go      | 282 ++++++++++++++-------
internal/config/config_test.go | 465 ------------------------------------
2 files changed, 183 insertions(+), 564 deletions(-)

Detailed changes

internal/config/config.go 🔗

@@ -1,21 +1,27 @@
+// Package config manages application configuration from various sources.
 package config
 
 import (
 	"fmt"
+	"log/slog"
 	"os"
 	"strings"
 
 	"github.com/kujtimiihoxha/termai/internal/llm/models"
+	"github.com/kujtimiihoxha/termai/internal/logging"
 	"github.com/spf13/viper"
 )
 
+// MCPType defines the type of MCP (Model Control Protocol) server.
 type MCPType string
 
+// Supported MCP types
 const (
 	MCPStdio MCPType = "stdio"
 	MCPSse   MCPType = "sse"
 )
 
+// MCPServer defines the configuration for a Model Control Protocol server.
 type MCPServer struct {
 	Command string            `json:"command"`
 	Env     []string          `json:"env"`
@@ -23,37 +29,28 @@ type MCPServer struct {
 	Type    MCPType           `json:"type"`
 	URL     string            `json:"url"`
 	Headers map[string]string `json:"headers"`
-	// TODO: add permissions configuration
-	// TODO: add the ability to specify the tools to import
 }
 
+// Model defines configuration for different LLM models and their token limits.
 type Model struct {
 	Coder          models.ModelID `json:"coder"`
 	CoderMaxTokens int64          `json:"coderMaxTokens"`
-
-	Task          models.ModelID `json:"task"`
-	TaskMaxTokens int64          `json:"taskMaxTokens"`
-	// TODO: Maybe support multiple models for different purposes
-}
-
-type AnthropicConfig struct {
-	DisableCache bool `json:"disableCache"`
-	UseBedrock   bool `json:"useBedrock"`
+	Task           models.ModelID `json:"task"`
+	TaskMaxTokens  int64          `json:"taskMaxTokens"`
 }
 
+// Provider defines configuration for an LLM provider.
 type Provider struct {
-	APIKey  string `json:"apiKey"`
-	Enabled bool   `json:"enabled"`
+	APIKey   string `json:"apiKey"`
+	Disabled bool   `json:"disabled"`
 }
 
+// Data defines storage configuration.
 type Data struct {
 	Directory string `json:"directory"`
 }
 
-type Log struct {
-	Level string `json:"level"`
-}
-
+// LSPConfig defines configuration for Language Server Protocol integration.
 type LSPConfig struct {
 	Disabled bool     `json:"enabled"`
 	Command  string   `json:"command"`
@@ -61,41 +58,88 @@ type LSPConfig struct {
 	Options  any      `json:"options"`
 }
 
+// Config is the main configuration structure for the application.
 type Config struct {
-	Data       *Data                             `json:"data,omitempty"`
-	Log        *Log                              `json:"log,omitempty"`
+	Data       Data                              `json:"data"`
+	WorkingDir string                            `json:"wd,omitempty"`
 	MCPServers map[string]MCPServer              `json:"mcpServers,omitempty"`
 	Providers  map[models.ModelProvider]Provider `json:"providers,omitempty"`
-
-	LSP map[string]LSPConfig `json:"lsp,omitempty"`
-
-	Model *Model `json:"model,omitempty"`
-
-	Debug bool `json:"debug,omitempty"`
+	LSP        map[string]LSPConfig              `json:"lsp,omitempty"`
+	Model      Model                             `json:"model"`
+	Debug      bool                              `json:"debug,omitempty"`
 }
 
-var cfg *Config
-
+// Application constants
 const (
-	defaultDataDirectory = ".termai"
+	defaultDataDirectory = ".opencode"
 	defaultLogLevel      = "info"
 	defaultMaxTokens     = int64(5000)
-	termai               = "termai"
+	appName              = "opencode"
 )
 
-func Load(debug bool) error {
+// Global configuration instance
+var cfg *Config
+
+// Load initializes the configuration from environment variables and config files.
+// If debug is true, debug mode is enabled and log level is set to debug.
+// It returns an error if configuration loading fails.
+func Load(workingDir string, debug bool) error {
 	if cfg != nil {
 		return nil
 	}
 
-	viper.SetConfigName(fmt.Sprintf(".%s", termai))
+	cfg = &Config{
+		WorkingDir: workingDir,
+		MCPServers: make(map[string]MCPServer),
+		Providers:  make(map[models.ModelProvider]Provider),
+		LSP:        make(map[string]LSPConfig),
+	}
+
+	configureViper()
+	setDefaults(debug)
+	setProviderDefaults()
+
+	// Read global config
+	if err := readConfig(viper.ReadInConfig()); err != nil {
+		return err
+	}
+
+	// Load and merge local config
+	mergeLocalConfig(workingDir)
+
+	// Apply configuration to the struct
+	if err := viper.Unmarshal(cfg); err != nil {
+		return err
+	}
+
+	applyDefaultValues()
+
+	defaultLevel := slog.LevelInfo
+	if cfg.Debug {
+		defaultLevel = slog.LevelDebug
+	}
+	// Configure logger
+	logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
+		Level: defaultLevel,
+	}))
+	slog.SetDefault(logger)
+	return nil
+}
+
+// configureViper sets up viper's configuration paths and environment variables.
+func configureViper() {
+	viper.SetConfigName(fmt.Sprintf(".%s", appName))
 	viper.SetConfigType("json")
 	viper.AddConfigPath("$HOME")
-	viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", termai))
-	viper.SetEnvPrefix(strings.ToUpper(termai))
+	viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
+	viper.SetEnvPrefix(strings.ToUpper(appName))
+	viper.AutomaticEnv()
+}
 
-	// Add defaults
+// setDefaults configures default values for configuration options.
+func setDefaults(debug bool) {
 	viper.SetDefault("data.directory", defaultDataDirectory)
+
 	if debug {
 		viper.SetDefault("debug", true)
 		viper.Set("log.level", "debug")
@@ -103,98 +147,138 @@ func Load(debug bool) error {
 		viper.SetDefault("debug", false)
 		viper.SetDefault("log.level", defaultLogLevel)
 	}
+}
+
+// setProviderDefaults configures LLM provider defaults based on environment variables.
+// the default model priority is:
+// 1. Anthropic
+// 2. OpenAI
+// 3. Google Gemini
+// 4. AWS Bedrock
+func setProviderDefaults() {
+	// Groq configuration
+	if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
+		viper.SetDefault("providers.groq.apiKey", apiKey)
+		viper.SetDefault("model.coder", models.QWENQwq)
+		viper.SetDefault("model.coderMaxTokens", defaultMaxTokens)
+		viper.SetDefault("model.task", models.QWENQwq)
+		viper.SetDefault("model.taskMaxTokens", defaultMaxTokens)
+	}
+
+	// Google Gemini configuration
+	if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
+		viper.SetDefault("providers.gemini.apiKey", apiKey)
+		viper.SetDefault("model.coder", models.GRMINI20Flash)
+		viper.SetDefault("model.coderMaxTokens", defaultMaxTokens)
+		viper.SetDefault("model.task", models.GRMINI20Flash)
+		viper.SetDefault("model.taskMaxTokens", defaultMaxTokens)
+	}
 
-	defaultModelSet := false
-	if os.Getenv("ANTHROPIC_API_KEY") != "" {
-		viper.SetDefault("providers.anthropic.apiKey", os.Getenv("ANTHROPIC_API_KEY"))
-		viper.SetDefault("providers.anthropic.enabled", true)
+	// OpenAI configuration
+	if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
+		viper.SetDefault("providers.openai.apiKey", apiKey)
+		viper.SetDefault("model.coder", models.GPT4o)
+		viper.SetDefault("model.coderMaxTokens", defaultMaxTokens)
+		viper.SetDefault("model.task", models.GPT4o)
+		viper.SetDefault("model.taskMaxTokens", defaultMaxTokens)
+	}
+
+	// Anthropic configuration
+	if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
+		viper.SetDefault("providers.anthropic.apiKey", apiKey)
 		viper.SetDefault("model.coder", models.Claude37Sonnet)
+		viper.SetDefault("model.coderMaxTokens", defaultMaxTokens)
 		viper.SetDefault("model.task", models.Claude37Sonnet)
-		defaultModelSet = true
-	}
-	if os.Getenv("OPENAI_API_KEY") != "" {
-		viper.SetDefault("providers.openai.apiKey", os.Getenv("OPENAI_API_KEY"))
-		viper.SetDefault("providers.openai.enabled", true)
-		if !defaultModelSet {
-			viper.SetDefault("model.coder", models.GPT41)
-			viper.SetDefault("model.task", models.GPT41)
-			defaultModelSet = true
-		}
+		viper.SetDefault("model.taskMaxTokens", defaultMaxTokens)
 	}
-	if os.Getenv("GEMINI_API_KEY") != "" {
-		viper.SetDefault("providers.gemini.apiKey", os.Getenv("GEMINI_API_KEY"))
-		viper.SetDefault("providers.gemini.enabled", true)
-		if !defaultModelSet {
-			viper.SetDefault("model.coder", models.GRMINI20Flash)
-			viper.SetDefault("model.task", models.GRMINI20Flash)
-			defaultModelSet = true
-		}
+
+	if hasAWSCredentials() {
+		viper.SetDefault("model.coder", models.BedrockClaude37Sonnet)
+		viper.SetDefault("model.coderMaxTokens", defaultMaxTokens)
+		viper.SetDefault("model.task", models.BedrockClaude37Sonnet)
+		viper.SetDefault("model.taskMaxTokens", defaultMaxTokens)
 	}
-	if os.Getenv("GROQ_API_KEY") != "" {
-		viper.SetDefault("providers.groq.apiKey", os.Getenv("GROQ_API_KEY"))
-		viper.SetDefault("providers.groq.enabled", true)
-		if !defaultModelSet {
-			viper.SetDefault("model.coder", models.QWENQwq)
-			viper.SetDefault("model.task", models.QWENQwq)
-			defaultModelSet = true
-		}
+}
+
+// hasAWSCredentials checks if AWS credentials are available in the environment.
+func hasAWSCredentials() bool {
+	// Check for explicit AWS credentials
+	if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
+		return true
 	}
 
-	viper.SetDefault("providers.bedrock.enabled", true)
-	// TODO: add more providers
-	cfg = &Config{}
+	// Check for AWS profile
+	if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
+		return true
+	}
 
-	err := viper.ReadInConfig()
-	if err != nil {
-		if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
-			return err
-		}
+	// Check for AWS region
+	if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" {
+		return true
 	}
-	local := viper.New()
-	local.SetConfigName(fmt.Sprintf(".%s", termai))
-	local.SetConfigType("json")
-	local.AddConfigPath(".")
-	// load local config, this will override the global config
-	if err = local.ReadInConfig(); err == nil {
-		viper.MergeConfigMap(local.AllSettings())
+
+	// Check if running on EC2 with instance profile
+	if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
+		os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
+		return true
+	}
+
+	return false
+}
+
+// readConfig handles the result of reading a configuration file.
+func readConfig(err error) error {
+	if err == nil {
+		return nil
 	}
-	viper.Unmarshal(cfg)
 
-	if cfg.Model != nil && cfg.Model.CoderMaxTokens <= 0 {
-		cfg.Model.CoderMaxTokens = defaultMaxTokens
+	// It's okay if the config file doesn't exist
+	if _, ok := err.(viper.ConfigFileNotFoundError); ok {
+		return nil
 	}
-	if cfg.Model != nil && cfg.Model.TaskMaxTokens <= 0 {
-		cfg.Model.TaskMaxTokens = defaultMaxTokens
+
+	return err
+}
+
+// mergeLocalConfig loads and merges configuration from the local directory.
+func mergeLocalConfig(workingDir string) {
+	local := viper.New()
+	local.SetConfigName(fmt.Sprintf(".%s", appName))
+	local.SetConfigType("json")
+	local.AddConfigPath(workingDir)
+
+	// Merge local config if it exists
+	if err := local.ReadInConfig(); err == nil {
+		viper.MergeConfigMap(local.AllSettings())
 	}
+}
 
-	for _, v := range cfg.MCPServers {
+// applyDefaultValues sets default values for configuration fields that need processing.
+func applyDefaultValues() {
+	// Set default MCP type if not specified
+	for k, v := range cfg.MCPServers {
 		if v.Type == "" {
 			v.Type = MCPStdio
+			cfg.MCPServers[k] = v
 		}
 	}
+}
 
+// setWorkingDirectory stores the current working directory in the configuration.
+func setWorkingDirectory() {
 	workdir, err := os.Getwd()
-	if err != nil {
-		return err
+	if err == nil {
+		viper.Set("wd", workdir)
 	}
-	viper.Set("wd", workdir)
-	return nil
 }
 
+// Get returns the current configuration.
+// It's safe to call this function multiple times.
 func Get() *Config {
-	if cfg == nil {
-		err := Load(false)
-		if err != nil {
-			panic(err)
-		}
-	}
 	return cfg
 }
 
+// WorkingDirectory returns the current working directory from the configuration.
 func WorkingDirectory() string {
 	return viper.GetString("wd")
 }
-
-func Write() error {
-	return viper.WriteConfig()
-}

internal/config/config_test.go 🔗

@@ -1,465 +0,0 @@
-package config
-
-import (
-	"fmt"
-	"os"
-	"path/filepath"
-	"testing"
-
-	"github.com/kujtimiihoxha/termai/internal/llm/models"
-	"github.com/spf13/viper"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-func TestLoad(t *testing.T) {
-	setupTest(t)
-
-	t.Run("loads configuration successfully", func(t *testing.T) {
-		homeDir := t.TempDir()
-		t.Setenv("HOME", homeDir)
-		configPath := filepath.Join(homeDir, ".termai.json")
-
-		configContent := `{
-			"data": {
-				"directory": "custom-dir"
-			},
-			"log": {
-				"level": "debug"
-			},
-			"mcpServers": {
-				"test-server": {
-					"command": "test-command",
-					"env": ["TEST_ENV=value"],
-					"args": ["--arg1", "--arg2"],
-					"type": "stdio",
-					"url": "",
-					"headers": {}
-				},
-				"sse-server": {
-					"command": "",
-					"env": [],
-					"args": [],
-					"type": "sse",
-					"url": "https://api.example.com/events",
-					"headers": {
-						"Authorization": "Bearer token123",
-						"Content-Type": "application/json"
-					}
-				}
-			},
-			"providers": {
-				"anthropic": {
-					"apiKey": "test-api-key",
-					"enabled": true
-				}
-			},
-			"model": {
-				"coder": "claude-3-haiku",
-				"task": "claude-3-haiku"
-			}
-		}`
-		err := os.WriteFile(configPath, []byte(configContent), 0o644)
-		require.NoError(t, err)
-
-		cfg = nil
-		viper.Reset()
-
-		err = Load(false)
-		require.NoError(t, err)
-
-		config := Get()
-		assert.NotNil(t, config)
-		assert.Equal(t, "custom-dir", config.Data.Directory)
-		assert.Equal(t, "debug", config.Log.Level)
-
-		assert.Contains(t, config.MCPServers, "test-server")
-		stdioServer := config.MCPServers["test-server"]
-		assert.Equal(t, "test-command", stdioServer.Command)
-		assert.Equal(t, []string{"TEST_ENV=value"}, stdioServer.Env)
-		assert.Equal(t, []string{"--arg1", "--arg2"}, stdioServer.Args)
-		assert.Equal(t, MCPStdio, stdioServer.Type)
-		assert.Equal(t, "", stdioServer.URL)
-		assert.Empty(t, stdioServer.Headers)
-
-		assert.Contains(t, config.MCPServers, "sse-server")
-		sseServer := config.MCPServers["sse-server"]
-		assert.Equal(t, "", sseServer.Command)
-		assert.Empty(t, sseServer.Env)
-		assert.Empty(t, sseServer.Args)
-		assert.Equal(t, MCPSse, sseServer.Type)
-		assert.Equal(t, "https://api.example.com/events", sseServer.URL)
-		assert.Equal(t, map[string]string{
-			"authorization": "Bearer token123",
-			"content-type":  "application/json",
-		}, sseServer.Headers)
-
-		assert.Contains(t, config.Providers, models.ModelProvider("anthropic"))
-		provider := config.Providers[models.ModelProvider("anthropic")]
-		assert.Equal(t, "test-api-key", provider.APIKey)
-		assert.True(t, provider.Enabled)
-
-		assert.NotNil(t, config.Model)
-		assert.Equal(t, models.Claude3Haiku, config.Model.Coder)
-		assert.Equal(t, models.Claude3Haiku, config.Model.Task)
-		assert.Equal(t, defaultMaxTokens, config.Model.CoderMaxTokens)
-	})
-
-	t.Run("loads configuration with environment variables", func(t *testing.T) {
-		homeDir := t.TempDir()
-		t.Setenv("HOME", homeDir)
-		configPath := filepath.Join(homeDir, ".termai.json")
-		err := os.WriteFile(configPath, []byte("{}"), 0o644)
-		require.NoError(t, err)
-
-		t.Setenv("ANTHROPIC_API_KEY", "env-anthropic-key")
-		t.Setenv("OPENAI_API_KEY", "env-openai-key")
-		t.Setenv("GEMINI_API_KEY", "env-gemini-key")
-
-		cfg = nil
-		viper.Reset()
-
-		err = Load(false)
-		require.NoError(t, err)
-
-		config := Get()
-		assert.NotNil(t, config)
-
-		assert.Equal(t, defaultDataDirectory, config.Data.Directory)
-		assert.Equal(t, defaultLogLevel, config.Log.Level)
-
-		assert.Contains(t, config.Providers, models.ModelProvider("anthropic"))
-		assert.Equal(t, "env-anthropic-key", config.Providers[models.ModelProvider("anthropic")].APIKey)
-		assert.True(t, config.Providers[models.ModelProvider("anthropic")].Enabled)
-
-		assert.Contains(t, config.Providers, models.ModelProvider("openai"))
-		assert.Equal(t, "env-openai-key", config.Providers[models.ModelProvider("openai")].APIKey)
-		assert.True(t, config.Providers[models.ModelProvider("openai")].Enabled)
-
-		assert.Contains(t, config.Providers, models.ModelProvider("gemini"))
-		assert.Equal(t, "env-gemini-key", config.Providers[models.ModelProvider("gemini")].APIKey)
-		assert.True(t, config.Providers[models.ModelProvider("gemini")].Enabled)
-
-		assert.Equal(t, models.Claude37Sonnet, config.Model.Coder)
-	})
-
-	t.Run("local config overrides global config", func(t *testing.T) {
-		homeDir := t.TempDir()
-		t.Setenv("HOME", homeDir)
-		globalConfigPath := filepath.Join(homeDir, ".termai.json")
-		globalConfig := `{
-			"data": {
-				"directory": "global-dir"
-			},
-			"log": {
-				"level": "info"
-			}
-		}`
-		err := os.WriteFile(globalConfigPath, []byte(globalConfig), 0o644)
-		require.NoError(t, err)
-
-		workDir := t.TempDir()
-		origDir, err := os.Getwd()
-		require.NoError(t, err)
-		defer os.Chdir(origDir)
-		err = os.Chdir(workDir)
-		require.NoError(t, err)
-
-		localConfigPath := filepath.Join(workDir, ".termai.json")
-		localConfig := `{
-			"data": {
-				"directory": "local-dir"
-			},
-			"log": {
-				"level": "debug"
-			}
-		}`
-		err = os.WriteFile(localConfigPath, []byte(localConfig), 0o644)
-		require.NoError(t, err)
-
-		cfg = nil
-		viper.Reset()
-
-		err = Load(false)
-		require.NoError(t, err)
-
-		config := Get()
-		assert.NotNil(t, config)
-
-		assert.Equal(t, "local-dir", config.Data.Directory)
-		assert.Equal(t, "debug", config.Log.Level)
-	})
-
-	t.Run("missing config file should not return error", func(t *testing.T) {
-		emptyDir := t.TempDir()
-		t.Setenv("HOME", emptyDir)
-
-		cfg = nil
-		viper.Reset()
-
-		err := Load(false)
-		assert.NoError(t, err)
-	})
-
-	t.Run("model priority and fallbacks", func(t *testing.T) {
-		testCases := []struct {
-			name             string
-			anthropicKey     string
-			openaiKey        string
-			geminiKey        string
-			expectedModel    models.ModelID
-			explicitModel    models.ModelID
-			useExplicitModel bool
-		}{
-			{
-				name:          "anthropic has priority",
-				anthropicKey:  "test-key",
-				openaiKey:     "test-key",
-				geminiKey:     "test-key",
-				expectedModel: models.Claude37Sonnet,
-			},
-			{
-				name:          "fallback to openai when no anthropic",
-				anthropicKey:  "",
-				openaiKey:     "test-key",
-				geminiKey:     "test-key",
-				expectedModel: models.GPT41,
-			},
-			{
-				name:          "fallback to gemini when no others",
-				anthropicKey:  "",
-				openaiKey:     "",
-				geminiKey:     "test-key",
-				expectedModel: models.GRMINI20Flash,
-			},
-			{
-				name:             "explicit model overrides defaults",
-				anthropicKey:     "test-key",
-				openaiKey:        "test-key",
-				geminiKey:        "test-key",
-				explicitModel:    models.GPT41,
-				useExplicitModel: true,
-				expectedModel:    models.GPT41,
-			},
-		}
-
-		for _, tc := range testCases {
-			t.Run(tc.name, func(t *testing.T) {
-				homeDir := t.TempDir()
-				t.Setenv("HOME", homeDir)
-				configPath := filepath.Join(homeDir, ".termai.json")
-
-				configContent := "{}"
-				if tc.useExplicitModel {
-					configContent = fmt.Sprintf(`{"model":{"coder":"%s"}}`, tc.explicitModel)
-				}
-
-				err := os.WriteFile(configPath, []byte(configContent), 0o644)
-				require.NoError(t, err)
-
-				if tc.anthropicKey != "" {
-					t.Setenv("ANTHROPIC_API_KEY", tc.anthropicKey)
-				} else {
-					t.Setenv("ANTHROPIC_API_KEY", "")
-				}
-
-				if tc.openaiKey != "" {
-					t.Setenv("OPENAI_API_KEY", tc.openaiKey)
-				} else {
-					t.Setenv("OPENAI_API_KEY", "")
-				}
-
-				if tc.geminiKey != "" {
-					t.Setenv("GEMINI_API_KEY", tc.geminiKey)
-				} else {
-					t.Setenv("GEMINI_API_KEY", "")
-				}
-
-				cfg = nil
-				viper.Reset()
-
-				err = Load(false)
-				require.NoError(t, err)
-
-				config := Get()
-				assert.NotNil(t, config)
-				assert.Equal(t, tc.expectedModel, config.Model.Coder)
-			})
-		}
-	})
-}
-
-func TestGet(t *testing.T) {
-	t.Run("get returns same config instance", func(t *testing.T) {
-		setupTest(t)
-		homeDir := t.TempDir()
-		t.Setenv("HOME", homeDir)
-		configPath := filepath.Join(homeDir, ".termai.json")
-		err := os.WriteFile(configPath, []byte("{}"), 0o644)
-		require.NoError(t, err)
-
-		cfg = nil
-		viper.Reset()
-
-		config1 := Get()
-		require.NotNil(t, config1)
-
-		config2 := Get()
-		require.NotNil(t, config2)
-
-		assert.Same(t, config1, config2)
-	})
-
-	t.Run("get loads config if not loaded", func(t *testing.T) {
-		setupTest(t)
-		homeDir := t.TempDir()
-		t.Setenv("HOME", homeDir)
-		configPath := filepath.Join(homeDir, ".termai.json")
-		configContent := `{"data":{"directory":"test-dir"}}`
-		err := os.WriteFile(configPath, []byte(configContent), 0o644)
-		require.NoError(t, err)
-
-		cfg = nil
-		viper.Reset()
-
-		config := Get()
-		require.NotNil(t, config)
-		assert.Equal(t, "test-dir", config.Data.Directory)
-	})
-}
-
-func TestWorkingDirectory(t *testing.T) {
-	t.Run("returns current working directory", func(t *testing.T) {
-		setupTest(t)
-		homeDir := t.TempDir()
-		t.Setenv("HOME", homeDir)
-		configPath := filepath.Join(homeDir, ".termai.json")
-		err := os.WriteFile(configPath, []byte("{}"), 0o644)
-		require.NoError(t, err)
-
-		cfg = nil
-		viper.Reset()
-
-		err = Load(false)
-		require.NoError(t, err)
-
-		wd := WorkingDirectory()
-		expectedWd, err := os.Getwd()
-		require.NoError(t, err)
-		assert.Equal(t, expectedWd, wd)
-	})
-}
-
-func TestWrite(t *testing.T) {
-	t.Run("writes config to file", func(t *testing.T) {
-		setupTest(t)
-		homeDir := t.TempDir()
-		t.Setenv("HOME", homeDir)
-		configPath := filepath.Join(homeDir, ".termai.json")
-		err := os.WriteFile(configPath, []byte("{}"), 0o644)
-		require.NoError(t, err)
-
-		cfg = nil
-		viper.Reset()
-
-		err = Load(false)
-		require.NoError(t, err)
-
-		viper.Set("data.directory", "modified-dir")
-
-		err = Write()
-		require.NoError(t, err)
-
-		content, err := os.ReadFile(configPath)
-		require.NoError(t, err)
-		assert.Contains(t, string(content), "modified-dir")
-	})
-}
-
-func TestMCPType(t *testing.T) {
-	t.Run("MCPType constants", func(t *testing.T) {
-		assert.Equal(t, MCPType("stdio"), MCPStdio)
-		assert.Equal(t, MCPType("sse"), MCPSse)
-	})
-
-	t.Run("MCPType JSON unmarshaling", func(t *testing.T) {
-		homeDir := t.TempDir()
-		t.Setenv("HOME", homeDir)
-		configPath := filepath.Join(homeDir, ".termai.json")
-
-		configContent := `{
-			"mcpServers": {
-				"stdio-server": {
-					"type": "stdio"
-				},
-				"sse-server": {
-					"type": "sse"
-				},
-				"invalid-server": {
-					"type": "invalid"
-				}
-			}
-		}`
-		err := os.WriteFile(configPath, []byte(configContent), 0o644)
-		require.NoError(t, err)
-
-		cfg = nil
-		viper.Reset()
-
-		err = Load(false)
-		require.NoError(t, err)
-
-		config := Get()
-		assert.NotNil(t, config)
-
-		assert.Equal(t, MCPStdio, config.MCPServers["stdio-server"].Type)
-		assert.Equal(t, MCPSse, config.MCPServers["sse-server"].Type)
-		assert.Equal(t, MCPType("invalid"), config.MCPServers["invalid-server"].Type)
-	})
-
-	t.Run("default MCPType", func(t *testing.T) {
-		homeDir := t.TempDir()
-		t.Setenv("HOME", homeDir)
-		configPath := filepath.Join(homeDir, ".termai.json")
-
-		configContent := `{
-			"mcpServers": {
-				"test-server": {
-					"command": "test-command"
-				}
-			}
-		}`
-		err := os.WriteFile(configPath, []byte(configContent), 0o644)
-		require.NoError(t, err)
-
-		cfg = nil
-		viper.Reset()
-
-		err = Load(false)
-		require.NoError(t, err)
-
-		config := Get()
-		assert.NotNil(t, config)
-
-		assert.Equal(t, MCPType(""), config.MCPServers["test-server"].Type)
-	})
-}
-
-func setupTest(t *testing.T) {
-	origHome := os.Getenv("HOME")
-	origXdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
-	origAnthropicKey := os.Getenv("ANTHROPIC_API_KEY")
-	origOpenAIKey := os.Getenv("OPENAI_API_KEY")
-	origGeminiKey := os.Getenv("GEMINI_API_KEY")
-
-	t.Cleanup(func() {
-		t.Setenv("HOME", origHome)
-		t.Setenv("XDG_CONFIG_HOME", origXdgConfigHome)
-		t.Setenv("ANTHROPIC_API_KEY", origAnthropicKey)
-		t.Setenv("OPENAI_API_KEY", origOpenAIKey)
-		t.Setenv("GEMINI_API_KEY", origGeminiKey)
-
-		cfg = nil
-		viper.Reset()
-	})
-}