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
 95	// TODO: maybe make it possible to get the value from the env
 96	Headers map[string]string `json:"headers,omitempty"`
 97}
 98
 99type LSPConfig struct {
100	Disabled bool     `json:"enabled,omitempty"`
101	Command  string   `json:"command"`
102	Args     []string `json:"args,omitempty"`
103	Options  any      `json:"options,omitempty"`
104}
105
106type TUIOptions struct {
107	CompactMode bool `json:"compact_mode,omitempty"`
108	// Here we can add themes later or any TUI related options
109}
110
111type Options struct {
112	ContextPaths         []string    `json:"context_paths,omitempty"`
113	TUI                  *TUIOptions `json:"tui,omitempty"`
114	Debug                bool        `json:"debug,omitempty"`
115	DebugLSP             bool        `json:"debug_lsp,omitempty"`
116	DisableAutoSummarize bool        `json:"disable_auto_summarize,omitempty"`
117	// Relative to the cwd
118	DataDirectory string `json:"data_directory,omitempty"`
119}
120
121type MCPs map[string]MCPConfig
122
123type MCP struct {
124	Name string    `json:"name"`
125	MCP  MCPConfig `json:"mcp"`
126}
127
128func (m MCPs) Sorted() []MCP {
129	sorted := make([]MCP, 0, len(m))
130	for k, v := range m {
131		sorted = append(sorted, MCP{
132			Name: k,
133			MCP:  v,
134		})
135	}
136	slices.SortFunc(sorted, func(a, b MCP) int {
137		return strings.Compare(a.Name, b.Name)
138	})
139	return sorted
140}
141
142type LSPs map[string]LSPConfig
143
144type LSP struct {
145	Name string    `json:"name"`
146	LSP  LSPConfig `json:"lsp"`
147}
148
149func (l LSPs) Sorted() []LSP {
150	sorted := make([]LSP, 0, len(l))
151	for k, v := range l {
152		sorted = append(sorted, LSP{
153			Name: k,
154			LSP:  v,
155		})
156	}
157	slices.SortFunc(sorted, func(a, b LSP) int {
158		return strings.Compare(a.Name, b.Name)
159	})
160	return sorted
161}
162
163type Agent struct {
164	ID          string `json:"id,omitempty"`
165	Name        string `json:"name,omitempty"`
166	Description string `json:"description,omitempty"`
167	// This is the id of the system prompt used by the agent
168	Disabled bool `json:"disabled,omitempty"`
169
170	Model SelectedModelType `json:"model"`
171
172	// The available tools for the agent
173	//  if this is nil, all tools are available
174	AllowedTools []string `json:"allowed_tools,omitempty"`
175
176	// this tells us which MCPs are available for this agent
177	//  if this is empty all mcps are available
178	//  the string array is the list of tools from the AllowedMCP the agent has available
179	//  if the string array is nil, all tools from the AllowedMCP are available
180	AllowedMCP map[string][]string `json:"allowed_mcp,omitempty"`
181
182	// The list of LSPs that this agent can use
183	//  if this is nil, all LSPs are available
184	AllowedLSP []string `json:"allowed_lsp,omitempty"`
185
186	// Overrides the context paths for this agent
187	ContextPaths []string `json:"context_paths,omitempty"`
188}
189
190// Config holds the configuration for crush.
191type Config struct {
192	// We currently only support large/small as values here.
193	Models map[SelectedModelType]SelectedModel `json:"models,omitempty"`
194
195	// The providers that are configured
196	Providers map[string]ProviderConfig `json:"providers,omitempty"`
197
198	MCP MCPs `json:"mcp,omitempty"`
199
200	LSP LSPs `json:"lsp,omitempty"`
201
202	Options *Options `json:"options,omitempty"`
203
204	// Internal
205	workingDir string `json:"-"`
206	// TODO: most likely remove this concept when I come back to it
207	Agents map[string]Agent `json:"-"`
208	// TODO: find a better way to do this this should probably not be part of the config
209	resolver VariableResolver
210}
211
212func (c *Config) WorkingDir() string {
213	return c.workingDir
214}
215
216func (c *Config) EnabledProviders() []ProviderConfig {
217	enabled := make([]ProviderConfig, 0, len(c.Providers))
218	for _, p := range c.Providers {
219		if !p.Disable {
220			enabled = append(enabled, p)
221		}
222	}
223	return enabled
224}
225
226// IsConfigured  return true if at least one provider is configured
227func (c *Config) IsConfigured() bool {
228	return len(c.EnabledProviders()) > 0
229}
230
231func (c *Config) GetModel(provider, model string) *provider.Model {
232	if providerConfig, ok := c.Providers[provider]; ok {
233		for _, m := range providerConfig.Models {
234			if m.ID == model {
235				return &m
236			}
237		}
238	}
239	return nil
240}
241
242func (c *Config) GetProviderForModel(modelType SelectedModelType) *ProviderConfig {
243	model, ok := c.Models[modelType]
244	if !ok {
245		return nil
246	}
247	if providerConfig, ok := c.Providers[model.Provider]; ok {
248		return &providerConfig
249	}
250	return nil
251}
252
253func (c *Config) GetModelByType(modelType SelectedModelType) *provider.Model {
254	model, ok := c.Models[modelType]
255	if !ok {
256		return nil
257	}
258	return c.GetModel(model.Provider, model.Model)
259}
260
261func (c *Config) LargeModel() *provider.Model {
262	model, ok := c.Models[SelectedModelTypeLarge]
263	if !ok {
264		return nil
265	}
266	return c.GetModel(model.Provider, model.Model)
267}
268
269func (c *Config) SmallModel() *provider.Model {
270	model, ok := c.Models[SelectedModelTypeSmall]
271	if !ok {
272		return nil
273	}
274	return c.GetModel(model.Provider, model.Model)
275}
276
277func (c *Config) Resolve(key string) (string, error) {
278	if c.resolver == nil {
279		return "", fmt.Errorf("no variable resolver configured")
280	}
281	return c.resolver.ResolveValue(key)
282}
283
284// TODO: maybe handle this better
285func UpdatePreferredModel(modelType SelectedModelType, model SelectedModel) error {
286	cfg := Get()
287	cfg.Models[modelType] = model
288	return nil
289}