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 AnthropicConfig struct {
 40	DisableCache bool `json:"disableCache"`
 41	UseBedrock   bool `json:"useBedrock"`
 42}
 43
 44type Provider struct {
 45	APIKey  string `json:"apiKey"`
 46	Enabled bool   `json:"enabled"`
 47}
 48
 49type Data struct {
 50	Directory string `json:"directory"`
 51}
 52
 53type Log struct {
 54	Level string `json:"level"`
 55}
 56
 57type LSPConfig struct {
 58	Disabled bool     `json:"enabled"`
 59	Command  string   `json:"command"`
 60	Args     []string `json:"args"`
 61	Options  any      `json:"options"`
 62}
 63
 64type Config struct {
 65	Data       *Data                             `json:"data,omitempty"`
 66	Log        *Log                              `json:"log,omitempty"`
 67	MCPServers map[string]MCPServer              `json:"mcpServers,omitempty"`
 68	Providers  map[models.ModelProvider]Provider `json:"providers,omitempty"`
 69
 70	LSP map[string]LSPConfig `json:"lsp,omitempty"`
 71
 72	Model *Model `json:"model,omitempty"`
 73
 74	Debug bool `json:"debug,omitempty"`
 75}
 76
 77var cfg *Config
 78
 79const (
 80	defaultDataDirectory = ".termai"
 81	defaultLogLevel      = "info"
 82	defaultMaxTokens     = int64(5000)
 83	termai               = "termai"
 84)
 85
 86func Load(debug bool) error {
 87	if cfg != nil {
 88		return nil
 89	}
 90
 91	viper.SetConfigName(fmt.Sprintf(".%s", termai))
 92	viper.SetConfigType("json")
 93	viper.AddConfigPath("$HOME")
 94	viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", termai))
 95	viper.SetEnvPrefix(strings.ToUpper(termai))
 96
 97	// Add defaults
 98	viper.SetDefault("data.directory", defaultDataDirectory)
 99	if debug {
100		viper.SetDefault("debug", true)
101		viper.Set("log.level", "debug")
102	} else {
103		viper.SetDefault("debug", false)
104		viper.SetDefault("log.level", defaultLogLevel)
105	}
106
107	defaultModelSet := false
108	if os.Getenv("ANTHROPIC_API_KEY") != "" {
109		viper.SetDefault("providers.anthropic.apiKey", os.Getenv("ANTHROPIC_API_KEY"))
110		viper.SetDefault("providers.anthropic.enabled", true)
111		viper.SetDefault("model.coder", models.Claude37Sonnet)
112		viper.SetDefault("model.task", models.Claude37Sonnet)
113		defaultModelSet = true
114	}
115	if os.Getenv("OPENAI_API_KEY") != "" {
116		viper.SetDefault("providers.openai.apiKey", os.Getenv("OPENAI_API_KEY"))
117		viper.SetDefault("providers.openai.enabled", true)
118		if !defaultModelSet {
119			viper.SetDefault("model.coder", models.GPT4o)
120			viper.SetDefault("model.task", models.GPT4o)
121			defaultModelSet = true
122		}
123	}
124	if os.Getenv("GEMINI_API_KEY") != "" {
125		viper.SetDefault("providers.gemini.apiKey", os.Getenv("GEMINI_API_KEY"))
126		viper.SetDefault("providers.gemini.enabled", true)
127		if !defaultModelSet {
128			viper.SetDefault("model.coder", models.GRMINI20Flash)
129			viper.SetDefault("model.task", models.GRMINI20Flash)
130			defaultModelSet = true
131		}
132	}
133	if os.Getenv("GROQ_API_KEY") != "" {
134		viper.SetDefault("providers.groq.apiKey", os.Getenv("GROQ_API_KEY"))
135		viper.SetDefault("providers.groq.enabled", true)
136		if !defaultModelSet {
137			viper.SetDefault("model.coder", models.QWENQwq)
138			viper.SetDefault("model.task", models.QWENQwq)
139			defaultModelSet = true
140		}
141	}
142
143	viper.SetDefault("providers.bedrock.enabled", true)
144	// TODO: add more providers
145	cfg = &Config{}
146
147	err := viper.ReadInConfig()
148	if err != nil {
149		if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
150			return err
151		}
152	}
153	local := viper.New()
154	local.SetConfigName(fmt.Sprintf(".%s", termai))
155	local.SetConfigType("json")
156	local.AddConfigPath(".")
157	// load local config, this will override the global config
158	if err = local.ReadInConfig(); err == nil {
159		viper.MergeConfigMap(local.AllSettings())
160	}
161	viper.Unmarshal(cfg)
162
163	if cfg.Model != nil && cfg.Model.CoderMaxTokens <= 0 {
164		cfg.Model.CoderMaxTokens = defaultMaxTokens
165	}
166	if cfg.Model != nil && cfg.Model.TaskMaxTokens <= 0 {
167		cfg.Model.TaskMaxTokens = defaultMaxTokens
168	}
169
170	for _, v := range cfg.MCPServers {
171		if v.Type == "" {
172			v.Type = MCPStdio
173		}
174	}
175
176	workdir, err := os.Getwd()
177	if err != nil {
178		return err
179	}
180	viper.Set("wd", workdir)
181	return nil
182}
183
184func Get() *Config {
185	if cfg == nil {
186		err := Load(false)
187		if err != nil {
188			panic(err)
189		}
190	}
191	return cfg
192}
193
194func WorkingDirectory() string {
195	return viper.GetString("wd")
196}
197
198func Write() error {
199	return viper.WriteConfig()
200}