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