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}