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