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
 75var cfg *Config
 76
 77const (
 78	defaultDataDirectory = ".termai"
 79	defaultLogLevel      = "info"
 80	defaultMaxTokens     = int64(5000)
 81	termai               = "termai"
 82)
 83
 84func Load(debug bool) error {
 85	if cfg != nil {
 86		return nil
 87	}
 88
 89	viper.SetConfigName(fmt.Sprintf(".%s", termai))
 90	viper.SetConfigType("json")
 91	viper.AddConfigPath("$HOME")
 92	viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", termai))
 93	viper.SetEnvPrefix(strings.ToUpper(termai))
 94
 95	// Add defaults
 96	viper.SetDefault("data.directory", defaultDataDirectory)
 97	if debug {
 98		viper.Set("log.level", "debug")
 99	} else {
100		viper.SetDefault("log.level", defaultLogLevel)
101	}
102
103	defaultModelSet := false
104	if os.Getenv("ANTHROPIC_API_KEY") != "" {
105		viper.SetDefault("providers.anthropic.apiKey", os.Getenv("ANTHROPIC_API_KEY"))
106		viper.SetDefault("providers.anthropic.enabled", true)
107		viper.SetDefault("model.coder", models.Claude37Sonnet)
108		viper.SetDefault("model.task", models.Claude37Sonnet)
109		defaultModelSet = true
110	}
111	if os.Getenv("OPENAI_API_KEY") != "" {
112		viper.SetDefault("providers.openai.apiKey", os.Getenv("OPENAI_API_KEY"))
113		viper.SetDefault("providers.openai.enabled", true)
114		if !defaultModelSet {
115			viper.SetDefault("model.coder", models.GPT4o)
116			viper.SetDefault("model.task", models.GPT4o)
117			defaultModelSet = true
118		}
119	}
120	if os.Getenv("GEMINI_API_KEY") != "" {
121		viper.SetDefault("providers.gemini.apiKey", os.Getenv("GEMINI_API_KEY"))
122		viper.SetDefault("providers.gemini.enabled", true)
123		if !defaultModelSet {
124			viper.SetDefault("model.coder", models.GRMINI20Flash)
125			viper.SetDefault("model.task", models.GRMINI20Flash)
126			defaultModelSet = true
127		}
128	}
129	if os.Getenv("GROQ_API_KEY") != "" {
130		viper.SetDefault("providers.groq.apiKey", os.Getenv("GROQ_API_KEY"))
131		viper.SetDefault("providers.groq.enabled", true)
132		if !defaultModelSet {
133			viper.SetDefault("model.coder", models.QWENQwq)
134			viper.SetDefault("model.task", models.QWENQwq)
135			defaultModelSet = true
136		}
137	}
138
139	viper.SetDefault("providers.bedrock.enabled", true)
140	// TODO: add more providers
141	cfg = &Config{}
142
143	err := viper.ReadInConfig()
144	if err != nil {
145		if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
146			return err
147		}
148	}
149	local := viper.New()
150	local.SetConfigName(fmt.Sprintf(".%s", termai))
151	local.SetConfigType("json")
152	local.AddConfigPath(".")
153	// load local config, this will override the global config
154	if err = local.ReadInConfig(); err == nil {
155		viper.MergeConfigMap(local.AllSettings())
156	}
157	viper.Unmarshal(cfg)
158
159	if cfg.Model != nil && cfg.Model.CoderMaxTokens <= 0 {
160		cfg.Model.CoderMaxTokens = defaultMaxTokens
161	}
162	if cfg.Model != nil && cfg.Model.TaskMaxTokens <= 0 {
163		cfg.Model.TaskMaxTokens = defaultMaxTokens
164	}
165
166	for _, v := range cfg.MCPServers {
167		if v.Type == "" {
168			v.Type = MCPStdio
169		}
170	}
171
172	workdir, err := os.Getwd()
173	if err != nil {
174		return err
175	}
176	viper.Set("wd", workdir)
177	return nil
178}
179
180func Get() *Config {
181	if cfg == nil {
182		err := Load(false)
183		if err != nil {
184			panic(err)
185		}
186	}
187	return cfg
188}
189
190func WorkingDirectory() string {
191	return viper.GetString("wd")
192}
193
194func Write() error {
195	return viper.WriteConfig()
196}