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}