config.go

  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}