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}