1package config
2
3import (
4 "fmt"
5 "os"
6 "slices"
7 "strings"
8
9 "github.com/charmbracelet/crush/internal/fur/provider"
10 "github.com/tidwall/sjson"
11)
12
13const (
14 appName = "crush"
15 defaultDataDirectory = ".crush"
16 defaultLogLevel = "info"
17)
18
19var defaultContextPaths = []string{
20 ".github/copilot-instructions.md",
21 ".cursorrules",
22 ".cursor/rules/",
23 "CLAUDE.md",
24 "CLAUDE.local.md",
25 "GEMINI.md",
26 "gemini.md",
27 "crush.md",
28 "crush.local.md",
29 "Crush.md",
30 "Crush.local.md",
31 "CRUSH.md",
32 "CRUSH.local.md",
33}
34
35type SelectedModelType string
36
37const (
38 SelectedModelTypeLarge SelectedModelType = "large"
39 SelectedModelTypeSmall SelectedModelType = "small"
40)
41
42type SelectedModel struct {
43 // The model id as used by the provider API.
44 // Required.
45 Model string `json:"model"`
46 // The model provider, same as the key/id used in the providers config.
47 // Required.
48 Provider string `json:"provider"`
49
50 // Only used by models that use the openai provider and need this set.
51 ReasoningEffort string `json:"reasoning_effort,omitempty"`
52
53 // Overrides the default model configuration.
54 MaxTokens int64 `json:"max_tokens,omitempty"`
55
56 // Used by anthropic models that can reason to indicate if the model should think.
57 Think bool `json:"think,omitempty"`
58}
59
60type ProviderConfig struct {
61 // The provider's id.
62 ID string `json:"id,omitempty"`
63 // The provider's API endpoint.
64 BaseURL string `json:"base_url,omitempty"`
65 // The provider type, e.g. "openai", "anthropic", etc. if empty it defaults to openai.
66 Type provider.Type `json:"type,omitempty"`
67 // The provider's API key.
68 APIKey string `json:"api_key,omitempty"`
69 // Marks the provider as disabled.
70 Disable bool `json:"disable,omitempty"`
71
72 // Extra headers to send with each request to the provider.
73 ExtraHeaders map[string]string
74
75 // Used to pass extra parameters to the provider.
76 ExtraParams map[string]string `json:"-"`
77
78 // The provider models
79 Models []provider.Model `json:"models,omitempty"`
80}
81
82type MCPType string
83
84const (
85 MCPStdio MCPType = "stdio"
86 MCPSse MCPType = "sse"
87 MCPHttp MCPType = "http"
88)
89
90type MCPConfig struct {
91 Command string `json:"command,omitempty" `
92 Env []string `json:"env,omitempty"`
93 Args []string `json:"args,omitempty"`
94 Type MCPType `json:"type"`
95 URL string `json:"url,omitempty"`
96
97 // TODO: maybe make it possible to get the value from the env
98 Headers map[string]string `json:"headers,omitempty"`
99}
100
101type LSPConfig struct {
102 Disabled bool `json:"enabled,omitempty"`
103 Command string `json:"command"`
104 Args []string `json:"args,omitempty"`
105 Options any `json:"options,omitempty"`
106}
107
108type TUIOptions struct {
109 CompactMode bool `json:"compact_mode,omitempty"`
110 // Here we can add themes later or any TUI related options
111}
112
113type Options struct {
114 ContextPaths []string `json:"context_paths,omitempty"`
115 TUI *TUIOptions `json:"tui,omitempty"`
116 Debug bool `json:"debug,omitempty"`
117 DebugLSP bool `json:"debug_lsp,omitempty"`
118 DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty"`
119 // Relative to the cwd
120 DataDirectory string `json:"data_directory,omitempty"`
121}
122
123type MCPs map[string]MCPConfig
124
125type MCP struct {
126 Name string `json:"name"`
127 MCP MCPConfig `json:"mcp"`
128}
129
130func (m MCPs) Sorted() []MCP {
131 sorted := make([]MCP, 0, len(m))
132 for k, v := range m {
133 sorted = append(sorted, MCP{
134 Name: k,
135 MCP: v,
136 })
137 }
138 slices.SortFunc(sorted, func(a, b MCP) int {
139 return strings.Compare(a.Name, b.Name)
140 })
141 return sorted
142}
143
144type LSPs map[string]LSPConfig
145
146type LSP struct {
147 Name string `json:"name"`
148 LSP LSPConfig `json:"lsp"`
149}
150
151func (l LSPs) Sorted() []LSP {
152 sorted := make([]LSP, 0, len(l))
153 for k, v := range l {
154 sorted = append(sorted, LSP{
155 Name: k,
156 LSP: v,
157 })
158 }
159 slices.SortFunc(sorted, func(a, b LSP) int {
160 return strings.Compare(a.Name, b.Name)
161 })
162 return sorted
163}
164
165type Agent struct {
166 ID string `json:"id,omitempty"`
167 Name string `json:"name,omitempty"`
168 Description string `json:"description,omitempty"`
169 // This is the id of the system prompt used by the agent
170 Disabled bool `json:"disabled,omitempty"`
171
172 Model SelectedModelType `json:"model"`
173
174 // The available tools for the agent
175 // if this is nil, all tools are available
176 AllowedTools []string `json:"allowed_tools,omitempty"`
177
178 // this tells us which MCPs are available for this agent
179 // if this is empty all mcps are available
180 // the string array is the list of tools from the AllowedMCP the agent has available
181 // if the string array is nil, all tools from the AllowedMCP are available
182 AllowedMCP map[string][]string `json:"allowed_mcp,omitempty"`
183
184 // The list of LSPs that this agent can use
185 // if this is nil, all LSPs are available
186 AllowedLSP []string `json:"allowed_lsp,omitempty"`
187
188 // Overrides the context paths for this agent
189 ContextPaths []string `json:"context_paths,omitempty"`
190}
191
192// Config holds the configuration for crush.
193type Config struct {
194 // We currently only support large/small as values here.
195 Models map[SelectedModelType]SelectedModel `json:"models,omitempty"`
196
197 // The providers that are configured
198 Providers map[string]ProviderConfig `json:"providers,omitempty"`
199
200 MCP MCPs `json:"mcp,omitempty"`
201
202 LSP LSPs `json:"lsp,omitempty"`
203
204 Options *Options `json:"options,omitempty"`
205
206 // Internal
207 workingDir string `json:"-"`
208 // TODO: most likely remove this concept when I come back to it
209 Agents map[string]Agent `json:"-"`
210 // TODO: find a better way to do this this should probably not be part of the config
211 resolver VariableResolver
212}
213
214func (c *Config) WorkingDir() string {
215 return c.workingDir
216}
217
218func (c *Config) EnabledProviders() []ProviderConfig {
219 enabled := make([]ProviderConfig, 0, len(c.Providers))
220 for _, p := range c.Providers {
221 if !p.Disable {
222 enabled = append(enabled, p)
223 }
224 }
225 return enabled
226}
227
228// IsConfigured return true if at least one provider is configured
229func (c *Config) IsConfigured() bool {
230 return len(c.EnabledProviders()) > 0
231}
232
233func (c *Config) GetModel(provider, model string) *provider.Model {
234 if providerConfig, ok := c.Providers[provider]; ok {
235 for _, m := range providerConfig.Models {
236 if m.ID == model {
237 return &m
238 }
239 }
240 }
241 return nil
242}
243
244func (c *Config) GetProviderForModel(modelType SelectedModelType) *ProviderConfig {
245 model, ok := c.Models[modelType]
246 if !ok {
247 return nil
248 }
249 if providerConfig, ok := c.Providers[model.Provider]; ok {
250 return &providerConfig
251 }
252 return nil
253}
254
255func (c *Config) GetModelByType(modelType SelectedModelType) *provider.Model {
256 model, ok := c.Models[modelType]
257 if !ok {
258 return nil
259 }
260 return c.GetModel(model.Provider, model.Model)
261}
262
263func (c *Config) LargeModel() *provider.Model {
264 model, ok := c.Models[SelectedModelTypeLarge]
265 if !ok {
266 return nil
267 }
268 return c.GetModel(model.Provider, model.Model)
269}
270
271func (c *Config) SmallModel() *provider.Model {
272 model, ok := c.Models[SelectedModelTypeSmall]
273 if !ok {
274 return nil
275 }
276 return c.GetModel(model.Provider, model.Model)
277}
278
279func (c *Config) SetCompactMode(enabled bool) error {
280 if c.Options == nil {
281 c.Options = &Options{}
282 }
283 c.Options.TUI.CompactMode = enabled
284 return c.SetConfigField("options.tui.compact_mode", enabled)
285}
286
287func (c *Config) Resolve(key string) (string, error) {
288 if c.resolver == nil {
289 return "", fmt.Errorf("no variable resolver configured")
290 }
291 return c.resolver.ResolveValue(key)
292}
293
294// TODO: maybe handle this better
295func UpdatePreferredModel(modelType SelectedModelType, model SelectedModel) error {
296 cfg := Get()
297 cfg.Models[modelType] = model
298 return nil
299}
300
301func (c *Config) SetConfigField(key string, value any) error {
302 configPath := GlobalConfigData()
303 // read the data
304 data, err := os.ReadFile(configPath)
305 if err != nil {
306 if os.IsNotExist(err) {
307 data = []byte("{}")
308 } else {
309 return fmt.Errorf("failed to read config file: %w", err)
310 }
311 }
312
313 newValue, err := sjson.Set(string(data), key, value)
314 if err != nil {
315 return fmt.Errorf("failed to set config field %s: %w", key, err)
316 }
317 if err := os.WriteFile(configPath, []byte(newValue), 0o644); err != nil {
318 return fmt.Errorf("failed to write config file: %w", err)
319 }
320 return nil
321}