config.go

  1package config
  2
  3import (
  4	"fmt"
  5	"os"
  6	"strings"
  7
  8	"github.com/kujtimiihoxha/termai/internal/llm/models"
  9	"github.com/spf13/viper"
 10)
 11
 12type MCPType string
 13
 14const (
 15	MCPStdio MCPType = "stdio"
 16	MCPSse   MCPType = "sse"
 17)
 18
 19type MCPServer struct {
 20	Command string            `json:"command"`
 21	Env     []string          `json:"env"`
 22	Args    []string          `json:"args"`
 23	Type    MCPType           `json:"type"`
 24	URL     string            `json:"url"`
 25	Headers map[string]string `json:"headers"`
 26	// TODO: add permissions configuration
 27	// TODO: add the ability to specify the tools to import
 28}
 29
 30type Model struct {
 31	Coder          models.ModelID `json:"coder"`
 32	CoderMaxTokens int64          `json:"coderMaxTokens"`
 33
 34	Task          models.ModelID `json:"task"`
 35	TaskMaxTokens int64          `json:"taskMaxTokens"`
 36	// TODO: Maybe support multiple models for different purposes
 37}
 38
 39type Provider struct {
 40	APIKey  string `json:"apiKey"`
 41	Enabled bool   `json:"enabled"`
 42}
 43
 44type Data struct {
 45	Directory string `json:"directory"`
 46}
 47
 48type Log struct {
 49	Level string `json:"level"`
 50}
 51
 52type LSPConfig struct {
 53	Disabled bool     `json:"enabled"`
 54	Command  string   `json:"command"`
 55	Args     []string `json:"args"`
 56	Options  any      `json:"options"`
 57}
 58
 59type Config struct {
 60	Data       *Data                             `json:"data,omitempty"`
 61	Log        *Log                              `json:"log,omitempty"`
 62	MCPServers map[string]MCPServer              `json:"mcpServers,omitempty"`
 63	Providers  map[models.ModelProvider]Provider `json:"providers,omitempty"`
 64
 65	LSP map[string]LSPConfig `json:"lsp,omitempty"`
 66
 67	Model *Model `json:"model,omitempty"`
 68
 69	Debug bool `json:"debug,omitempty"`
 70}
 71
 72var cfg *Config
 73
 74const (
 75	defaultDataDirectory = ".termai"
 76	defaultLogLevel      = "info"
 77	defaultMaxTokens     = int64(5000)
 78	termai               = "termai"
 79)
 80
 81func Load(debug bool) error {
 82	if cfg != nil {
 83		return nil
 84	}
 85
 86	viper.SetConfigName(fmt.Sprintf(".%s", termai))
 87	viper.SetConfigType("json")
 88	viper.AddConfigPath("$HOME")
 89	viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", termai))
 90	viper.SetEnvPrefix(strings.ToUpper(termai))
 91
 92	// Add defaults
 93	viper.SetDefault("data.directory", defaultDataDirectory)
 94	if debug {
 95		viper.SetDefault("debug", true)
 96		viper.Set("log.level", "debug")
 97	} else {
 98		viper.SetDefault("debug", false)
 99		viper.SetDefault("log.level", defaultLogLevel)
100	}
101
102	defaultModelSet := false
103	if os.Getenv("ANTHROPIC_API_KEY") != "" {
104		viper.SetDefault("providers.anthropic.apiKey", os.Getenv("ANTHROPIC_API_KEY"))
105		viper.SetDefault("providers.anthropic.enabled", true)
106		viper.SetDefault("model.coder", models.Claude37Sonnet)
107		viper.SetDefault("model.task", models.Claude37Sonnet)
108		defaultModelSet = true
109	}
110	if os.Getenv("OPENAI_API_KEY") != "" {
111		viper.SetDefault("providers.openai.apiKey", os.Getenv("OPENAI_API_KEY"))
112		viper.SetDefault("providers.openai.enabled", true)
113		if !defaultModelSet {
114			viper.SetDefault("model.coder", models.GPT4o)
115			viper.SetDefault("model.task", models.GPT4o)
116			defaultModelSet = true
117		}
118	}
119	if os.Getenv("GEMINI_API_KEY") != "" {
120		viper.SetDefault("providers.gemini.apiKey", os.Getenv("GEMINI_API_KEY"))
121		viper.SetDefault("providers.gemini.enabled", true)
122		if !defaultModelSet {
123			viper.SetDefault("model.coder", models.GRMINI20Flash)
124			viper.SetDefault("model.task", models.GRMINI20Flash)
125			defaultModelSet = true
126		}
127	}
128	if os.Getenv("GROQ_API_KEY") != "" {
129		viper.SetDefault("providers.groq.apiKey", os.Getenv("GROQ_API_KEY"))
130		viper.SetDefault("providers.groq.enabled", true)
131		if !defaultModelSet {
132			viper.SetDefault("model.coder", models.QWENQwq)
133			viper.SetDefault("model.task", models.QWENQwq)
134			defaultModelSet = true
135		}
136	}
137	// TODO: add more providers
138	cfg = &Config{}
139
140	err := viper.ReadInConfig()
141	if err != nil {
142		if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
143			return err
144		}
145	}
146	local := viper.New()
147	local.SetConfigName(fmt.Sprintf(".%s", termai))
148	local.SetConfigType("json")
149	local.AddConfigPath(".")
150	// load local config, this will override the global config
151	if err = local.ReadInConfig(); err == nil {
152		viper.MergeConfigMap(local.AllSettings())
153	}
154	viper.Unmarshal(cfg)
155
156	if cfg.Model != nil && cfg.Model.CoderMaxTokens <= 0 {
157		cfg.Model.CoderMaxTokens = defaultMaxTokens
158	}
159	if cfg.Model != nil && cfg.Model.TaskMaxTokens <= 0 {
160		cfg.Model.TaskMaxTokens = defaultMaxTokens
161	}
162
163	for _, v := range cfg.MCPServers {
164		if v.Type == "" {
165			v.Type = MCPStdio
166		}
167	}
168
169	workdir, err := os.Getwd()
170	if err != nil {
171		return err
172	}
173	viper.Set("wd", workdir)
174	return nil
175}
176
177func Get() *Config {
178	if cfg == nil {
179		err := Load(false)
180		if err != nil {
181			panic(err)
182		}
183	}
184	return cfg
185}
186
187func WorkingDirectory() string {
188	return viper.GetString("wd")
189}
190
191func Write() error {
192	return viper.WriteConfig()
193}