@@ -10,8 +10,8 @@ import (
1010 "strings"
1111 "sync"
1212
13+ "github.com/charmbracelet/crush/internal/fur/provider"
1314 "github.com/charmbracelet/crush/internal/logging"
14− "github.com/charmbracelet/fur/pkg/provider"
1515)
1616
1717const (
@@ -22,6 +22,29 @@ const (
2222 MaxTokensFallbackDefault = 4096
2323)
2424
25+var defaultContextPaths = []string{
26+ ".github/copilot-instructions.md",
27+ ".cursorrules",
28+ ".cursor/rules/",
29+ "CLAUDE.md",
30+ "CLAUDE.local.md",
31+ "crush.md",
32+ "crush.local.md",
33+ "Crush.md",
34+ "Crush.local.md",
35+ "CRUSH.md",
36+ "CRUSH.local.md",
37+}
38+
39+type AgentID string
40+
41+const (
42+ AgentCoder AgentID = "coder"
43+ AgentTask AgentID = "task"
44+ AgentTitle AgentID = "title"
45+ AgentSummarize AgentID = "summarize"
46+)
47+
2548type Model struct {
2649 ID string `json:"id"`
2750 Name string `json:"model"`
@@ -43,40 +66,43 @@ type VertexAIOptions struct {
4366}
4467
4568type ProviderConfig struct {
46− BaseURL string `json:"base_url,omitempty"`
47− ProviderType provider.Type `json:"provider_type"`
48− APIKey string `json:"api_key,omitempty"`
49− Disabled bool `json:"disabled"`
50− ExtraHeaders map[string]string `json:"extra_headers,omitempty"`
69+ ID provider.InferenceProvider `json:"id"`
70+ BaseURL string `json:"base_url,omitempty"`
71+ ProviderType provider.Type `json:"provider_type"`
72+ APIKey string `json:"api_key,omitempty"`
73+ Disabled bool `json:"disabled"`
74+ ExtraHeaders map[string]string `json:"extra_headers,omitempty"`
5175 // used for e.x for vertex to set the project
5276 ExtraParams map[string]string `json:"extra_params,omitempty"`
5377
54− DefaultModel string `json:"default_model"`
78+ DefaultLargeModel string `json:"default_large_model,omitempty"`
79+ DefaultSmallModel string `json:"default_small_model,omitempty"`
80+
81+ Models []Model `json:"models,omitempty"`
5582}
5683
5784type Agent struct {
58− Name string `json:"name"`
85+ Name string `json:"name"`
86+ Description string `json:"description,omitempty"`
5987 // This is the id of the system prompt used by the agent
60− // TODO: still needs to be implemented
61− PromptID string `json:"prompt_id"`
62− Disabled bool `json:"disabled"`
88+ Disabled bool `json:"disabled"`
6389
6490 Provider provider.InferenceProvider `json:"provider"`
65− Model Model `json:"model"`
91+ Model string `json:"model"`
6692
6793 // The available tools for the agent
68− // if this is empty, all tools are available
94+ // if this is nil, all tools are available
6995 AllowedTools []string `json:"allowed_tools"`
7096
7197 // this tells us which MCPs are available for this agent
7298 // if this is empty all mcps are available
73− // the string array is the list of tools from the MCP the agent has available
74− // if the string array is empty, all tools from the MCP are available
75− MCP map[string][]string `json:"mcp"`
99+ // the string array is the list of tools from the AllowedMCP the agent has available
100+ // if the string array is nil, all tools from the AllowedMCP are available
101+ AllowedMCP map[string][]string `json:"allowed_mcp"`
76102
77103 // The list of LSPs that this agent can use
78− // if this is empty, all LSPs are available
79− LSP []string `json:"lsp"`
104+ // if this is nil, all LSPs are available
105+ AllowedLSP []string `json:"allowed_lsp"`
80106
81107 // Overrides the context paths for this agent
82108 ContextPaths []string `json:"context_paths"`
@@ -125,7 +151,7 @@ type Config struct {
125151 Providers map[provider.InferenceProvider]ProviderConfig `json:"providers,omitempty"`
126152
127153 // List of configured agents
128− Agents map[string]Agent `json:"agents,omitempty"`
154+ Agents map[AgentID]Agent `json:"agents,omitempty"`
129155
130156 // List of configured MCPs
131157 MCP map[string]MCP `json:"mcp,omitempty"`
@@ -135,15 +161,13 @@ type Config struct {
135161
136162 // Miscellaneous options
137163 Options Options `json:"options"`
138−
139− // Used to add models that are not already in the repository
140− Models map[provider.InferenceProvider][]provider.Model `json:"models,omitempty"`
141164}
142165
143166var (
144167 instance *Config // The single instance of the Singleton
145168 cwd string
146169 once sync.Once // Ensures the initialization happens only once
170+
147171)
148172
149173func loadConfig(cwd string) (*Config, error) {
@@ -190,10 +214,73 @@ func loadConfig(cwd string) (*Config, error) {
190214 }
191215
192216 // merge options
193− cfg.Options = mergeOptions(cfg.Options, globalCfg.Options)
194− cfg.Options = mergeOptions(cfg.Options, localConfig.Options)
217+ mergeOptions(cfg, globalCfg, localConfig)
195218
196219 mergeProviderConfigs(cfg, globalCfg, localConfig)
220+ // no providers found the app is not initialized yet
221+ if len(cfg.Providers) == 0 {
222+ return cfg, nil
223+ }
224+ preferredProvider := getPreferredProvider(cfg.Providers)
225+
226+ if preferredProvider == nil {
227+ return nil, errors.New("no valid providers configured")
228+ }
229+
230+ agents := map[AgentID]Agent{
231+ AgentCoder: {
232+ Name: "Coder",
233+ Description: "An agent that helps with executing coding tasks.",
234+ Provider: preferredProvider.ID,
235+ Model: preferredProvider.DefaultLargeModel,
236+ ContextPaths: cfg.Options.ContextPaths,
237+ // All tools allowed
238+ },
239+ AgentTask: {
240+ Name: "Task",
241+ Description: "An agent that helps with searching for context and finding implementation details.",
242+ Provider: preferredProvider.ID,
243+ Model: preferredProvider.DefaultLargeModel,
244+ ContextPaths: cfg.Options.ContextPaths,
245+ AllowedTools: []string{
246+ "glob",
247+ "grep",
248+ "ls",
249+ "sourcegraph",
250+ "view",
251+ },
252+ // NO MCPs or LSPs by default
253+ AllowedMCP: map[string][]string{},
254+ AllowedLSP: []string{},
255+ },
256+ AgentTitle: {
257+ Name: "Title",
258+ Description: "An agent that helps with generating titles for sessions.",
259+ Provider: preferredProvider.ID,
260+ Model: preferredProvider.DefaultSmallModel,
261+ ContextPaths: cfg.Options.ContextPaths,
262+ AllowedTools: []string{},
263+ // NO MCPs or LSPs by default
264+ AllowedMCP: map[string][]string{},
265+ AllowedLSP: []string{},
266+ },
267+ AgentSummarize: {
268+ Name: "Summarize",
269+ Description: "An agent that helps with summarizing sessions.",
270+ Provider: preferredProvider.ID,
271+ Model: preferredProvider.DefaultSmallModel,
272+ ContextPaths: cfg.Options.ContextPaths,
273+ AllowedTools: []string{},
274+ // NO MCPs or LSPs by default
275+ AllowedMCP: map[string][]string{},
276+ AllowedLSP: []string{},
277+ },
278+ }
279+ cfg.Agents = agents
280+ mergeAgents(cfg, globalCfg, localConfig)
281+ mergeMCPs(cfg, globalCfg, localConfig)
282+ mergeLSPs(cfg, globalCfg, localConfig)
283+
197284 return cfg, nil
198285}
199286
@@ -219,6 +306,22 @@ func GetConfig() *Config {
219306 return instance
220307}
221308
309+func getPreferredProvider(configuredProviders map[provider.InferenceProvider]ProviderConfig) *ProviderConfig {
310+ providers := Providers()
311+ for _, p := range providers {
312+ if providerConfig, ok := configuredProviders[p.ID]; ok && !providerConfig.Disabled {
313+ return &providerConfig
314+ }
315+ }
316+ // if none found return the first configured provider
317+ for _, providerConfig := range configuredProviders {
318+ if !providerConfig.Disabled {
319+ return &providerConfig
320+ }
321+ }
322+ return nil
323+}
324+
222325func mergeProviderConfig(p provider.InferenceProvider, base, other ProviderConfig) ProviderConfig {
223326 if other.APIKey != "" {
224327 base.APIKey = other.APIKey
@@ -249,6 +352,26 @@ func mergeProviderConfig(p provider.InferenceProvider, base, other ProviderConfi
249352 base.Disabled = other.Disabled
250353 }
251354
355+ if other.DefaultLargeModel != "" {
356+ base.DefaultLargeModel = other.DefaultLargeModel
357+ }
358+ // Add new models if they don't exist
359+ if other.Models != nil {
360+ for _, model := range other.Models {
361+ // check if the model already exists
362+ exists := false
363+ for _, existingModel := range base.Models {
364+ if existingModel.ID == model.ID {
365+ exists = true
366+ break
367+ }
368+ }
369+ if !exists {
370+ base.Models = append(base.Models, model)
371+ }
372+ }
373+ }
374+
252375 return base
253376}
254377
@@ -267,39 +390,114 @@ func validateProvider(p provider.InferenceProvider, providerConfig ProviderConfi
267390 return nil
268391}
269392
270−func mergeOptions(base, other Options) Options {
271− result := base
393+func mergeOptions(base, global, local *Config) {
394+ for _, cfg := range []*Config{global, local} {
395+ if cfg == nil {
396+ continue
397+ }
398+ baseOptions := base.Options
399+ other := cfg.Options
400+ if len(other.ContextPaths) > 0 {
401+ baseOptions.ContextPaths = append(baseOptions.ContextPaths, other.ContextPaths...)
402+ }
272403
273− if len(other.ContextPaths) > 0 {
274− base.ContextPaths = append(base.ContextPaths, other.ContextPaths...)
275− }
404+ if other.TUI.CompactMode {
405+ baseOptions.TUI.CompactMode = other.TUI.CompactMode
406+ }
276407
277− if other.TUI.CompactMode {
278− result.TUI.CompactMode = other.TUI.CompactMode
279− }
408+ if other.Debug {
409+ baseOptions.Debug = other.Debug
410+ }
280411
281− if other.Debug {
282− result.Debug = other.Debug
283− }
412+ if other.DebugLSP {
413+ baseOptions.DebugLSP = other.DebugLSP
414+ }
284415
285− if other.DebugLSP {
286− result.DebugLSP = other.DebugLSP
416+ if other.DisableAutoSummarize {
417+ baseOptions.DisableAutoSummarize = other.DisableAutoSummarize
418+ }
419+
420+ if other.DataDirectory != "" {
421+ baseOptions.DataDirectory = other.DataDirectory
422+ }
423+ base.Options = baseOptions
287424 }
425+}
288426
289− if other.DisableAutoSummarize {
290− result.DisableAutoSummarize = other.DisableAutoSummarize
427+func mergeAgents(base, global, local *Config) {
428+ for _, cfg := range []*Config{global, local} {
429+ if cfg == nil {
430+ continue
431+ }
432+ for agentID, globalAgent := range cfg.Agents {
433+ if _, ok := base.Agents[agentID]; !ok {
434+ base.Agents[agentID] = globalAgent
435+ } else {
436+ switch agentID {
437+ case AgentCoder:
438+ baseAgent := base.Agents[agentID]
439+ baseAgent.Model = globalAgent.Model
440+ baseAgent.Provider = globalAgent.Provider
441+ baseAgent.AllowedMCP = globalAgent.AllowedMCP
442+ baseAgent.AllowedLSP = globalAgent.AllowedLSP
443+ base.Agents[agentID] = baseAgent
444+ case AgentTask:
445+ baseAgent := base.Agents[agentID]
446+ baseAgent.Model = globalAgent.Model
447+ baseAgent.Provider = globalAgent.Provider
448+ base.Agents[agentID] = baseAgent
449+ case AgentTitle:
450+ baseAgent := base.Agents[agentID]
451+ baseAgent.Model = globalAgent.Model
452+ baseAgent.Provider = globalAgent.Provider
453+ base.Agents[agentID] = baseAgent
454+ case AgentSummarize:
455+ baseAgent := base.Agents[agentID]
456+ baseAgent.Model = globalAgent.Model
457+ baseAgent.Provider = globalAgent.Provider
458+ base.Agents[agentID] = baseAgent
459+ default:
460+ baseAgent := base.Agents[agentID]
461+ baseAgent.Name = globalAgent.Name
462+ baseAgent.Description = globalAgent.Description
463+ baseAgent.Disabled = globalAgent.Disabled
464+ baseAgent.Provider = globalAgent.Provider
465+ baseAgent.Model = globalAgent.Model
466+ baseAgent.AllowedTools = globalAgent.AllowedTools
467+ baseAgent.AllowedMCP = globalAgent.AllowedMCP
468+ baseAgent.AllowedLSP = globalAgent.AllowedLSP
469+ base.Agents[agentID] = baseAgent
470+
471+ }
472+ }
473+ }
291474 }
475+}
292476
293− if other.DataDirectory != "" {
294− result.DataDirectory = other.DataDirectory
477+func mergeMCPs(base, global, local *Config) {
478+ for _, cfg := range []*Config{global, local} {
479+ if cfg == nil {
480+ continue
481+ }
482+ maps.Copy(base.MCP, cfg.MCP)
295483 }
484+}
296485
297− return result
486+func mergeLSPs(base, global, local *Config) {
487+ for _, cfg := range []*Config{global, local} {
488+ if cfg == nil {
489+ continue
490+ }
491+ maps.Copy(base.LSP, cfg.LSP)
492+ }
298493}
299494
300495func mergeProviderConfigs(base, global, local *Config) {
301− if global != nil {
302− for providerName, globalProvider := range global.Providers {
496+ for _, cfg := range []*Config{global, local} {
497+ if cfg == nil {
498+ continue
499+ }
500+ for providerName, globalProvider := range cfg.Providers {
303501 if _, ok := base.Providers[providerName]; !ok {
304502 base.Providers[providerName] = globalProvider
305503 } else {
@@ -307,15 +505,6 @@ func mergeProviderConfigs(base, global, local *Config) {
307505 }
308506 }
309507 }
310− if local != nil {
311− for providerName, localProvider := range local.Providers {
312− if _, ok := base.Providers[providerName]; !ok {
313− base.Providers[providerName] = localProvider
314− } else {
315− base.Providers[providerName] = mergeProviderConfig(providerName, base.Providers[providerName], localProvider)
316− }
317− }
318− }
319508
320509 finalProviders := make(map[provider.InferenceProvider]ProviderConfig)
321510 for providerName, providerConfig := range base.Providers {
@@ -328,30 +517,36 @@ func mergeProviderConfigs(base, global, local *Config) {
328517 base.Providers = finalProviders
329518}
330519
331−func providerDefaultConfig(providerName provider.InferenceProvider) ProviderConfig {
332− switch providerName {
520+func providerDefaultConfig(providerId provider.InferenceProvider) ProviderConfig {
521+ switch providerId {
333522 case provider.InferenceProviderAnthropic:
334523 return ProviderConfig{
524+ ID: providerId,
335525 ProviderType: provider.TypeAnthropic,
336526 }
337527 case provider.InferenceProviderOpenAI:
338528 return ProviderConfig{
529+ ID: providerId,
339530 ProviderType: provider.TypeOpenAI,
340531 }
341532 case provider.InferenceProviderGemini:
342533 return ProviderConfig{
534+ ID: providerId,
343535 ProviderType: provider.TypeGemini,
344536 }
345537 case provider.InferenceProviderBedrock:
346538 return ProviderConfig{
539+ ID: providerId,
347540 ProviderType: provider.TypeBedrock,
348541 }
349542 case provider.InferenceProviderAzure:
350543 return ProviderConfig{
544+ ID: providerId,
351545 ProviderType: provider.TypeAzure,
352546 }
353547 case provider.InferenceProviderOpenRouter:
354548 return ProviderConfig{
549+ ID: providerId,
355550 ProviderType: provider.TypeOpenAI,
356551 BaseURL: "https://openrouter.ai/api/v1",
357552 ExtraHeaders: map[string]string{
@@ -361,15 +556,18 @@ func providerDefaultConfig(providerName provider.InferenceProvider) ProviderConf
361556 }
362557 case provider.InferenceProviderXAI:
363558 return ProviderConfig{
559+ ID: providerId,
364560 ProviderType: provider.TypeXAI,
365561 BaseURL: "https://api.x.ai/v1",
366562 }
367563 case provider.InferenceProviderVertexAI:
368564 return ProviderConfig{
565+ ID: providerId,
369566 ProviderType: provider.TypeVertexAI,
370567 }
371568 default:
372569 return ProviderConfig{
570+ ID: providerId,
373571 ProviderType: provider.TypeOpenAI,
374572 }
375573 }
@@ -379,6 +577,7 @@ func defaultConfigBasedOnEnv() *Config {
379577 cfg := &Config{
380578 Options: Options{
381579 DataDirectory: defaultDataDirectory,
580+ ContextPaths: defaultContextPaths,
382581 },
383582 Providers: make(map[provider.InferenceProvider]ProviderConfig),
384583 }
@@ -391,7 +590,22 @@ func defaultConfigBasedOnEnv() *Config {
391590 if apiKey := os.Getenv(envVar); apiKey != "" {
392591 providerConfig := providerDefaultConfig(p.ID)
393592 providerConfig.APIKey = apiKey
394− providerConfig.DefaultModel = p.DefaultModelID
593+ providerConfig.DefaultLargeModel = p.DefaultLargeModelID
594+ providerConfig.DefaultSmallModel = p.DefaultSmallModelID
595+ for _, model := range p.Models {
596+ providerConfig.Models = append(providerConfig.Models, Model{
597+ ID: model.ID,
598+ Name: model.Name,
599+ CostPer1MIn: model.CostPer1MIn,
600+ CostPer1MOut: model.CostPer1MOut,
601+ CostPer1MInCached: model.CostPer1MInCached,
602+ CostPer1MOutCached: model.CostPer1MOutCached,
603+ ContextWindow: model.ContextWindow,
604+ DefaultMaxTokens: model.DefaultMaxTokens,
605+ CanReason: model.CanReason,
606+ SupportsImages: model.SupportsImages,
607+ })
608+ }
395609 cfg.Providers[p.ID] = providerConfig
396610 }
397611 }