@@ -0,0 +1,440 @@
13+ "github.com/charmbracelet/crush/internal/logging"
14+ "github.com/charmbracelet/fur/pkg/provider"
18+ defaultDataDirectory = ".crush"
19+ defaultLogLevel = "info"
22+ MaxTokensFallbackDefault = 4096
26+ ID string `json:"id"`
27+ Name string `json:"model"`
28+ CostPer1MIn float64 `json:"cost_per_1m_in"`
29+ CostPer1MOut float64 `json:"cost_per_1m_out"`
30+ CostPer1MInCached float64 `json:"cost_per_1m_in_cached"`
31+ CostPer1MOutCached float64 `json:"cost_per_1m_out_cached"`
32+ ContextWindow int64 `json:"context_window"`
33+ DefaultMaxTokens int64 `json:"default_max_tokens"`
34+ CanReason bool `json:"can_reason"`
35+ ReasoningEffort string `json:"reasoning_effort"`
36+ SupportsImages bool `json:"supports_attachments"`
39+type VertexAIOptions struct {
40+ APIKey string `json:"api_key,omitempty"`
41+ Project string `json:"project,omitempty"`
42+ Location string `json:"location,omitempty"`
45+type 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"`
51+ // used for e.x for vertex to set the project
52+ ExtraParams map[string]string `json:"extra_params,omitempty"`
54+ DefaultModel string `json:"default_model"`
58+ Name string `json:"name"`
59+ // 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"`
64+ Provider provider.InferenceProvider `json:"provider"`
65+ Model Model `json:"model"`
67+ // The available tools for the agent
68+ // if this is empty, all tools are available
69+ AllowedTools []string `json:"allowed_tools"`
71+ // this tells us which MCPs are available for this agent
72+ // 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"`
77+ // The list of LSPs that this agent can use
78+ // if this is empty, all LSPs are available
79+ LSP []string `json:"lsp"`
81+ // Overrides the context paths for this agent
82+ ContextPaths []string `json:"context_paths"`
88+ MCPStdio MCPType = "stdio"
89+ MCPSse MCPType = "sse"
93+ Command string `json:"command"`
94+ Env []string `json:"env"`
95+ Args []string `json:"args"`
96+ Type MCPType `json:"type"`
97+ URL string `json:"url"`
98+ Headers map[string]string `json:"headers"`
101+type LSPConfig struct {
102+ Disabled bool `json:"enabled"`
103+ Command string `json:"command"`
104+ Args []string `json:"args"`
105+ Options any `json:"options"`
108+type TUIOptions struct {
109+ CompactMode bool `json:"compact_mode"`
110+ // Here we can add themes later or any TUI related options
113+type Options struct {
114+ ContextPaths []string `json:"context_paths"`
115+ TUI TUIOptions `json:"tui"`
116+ Debug bool `json:"debug"`
117+ DebugLSP bool `json:"debug_lsp"`
118+ DisableAutoSummarize bool `json:"disable_auto_summarize"`
119+ // Relative to the cwd
120+ DataDirectory string `json:"data_directory"`
124+ // List of configured providers
125+ Providers map[provider.InferenceProvider]ProviderConfig `json:"providers,omitempty"`
127+ // List of configured agents
128+ Agents map[string]Agent `json:"agents,omitempty"`
130+ // List of configured MCPs
131+ MCP map[string]MCP `json:"mcp,omitempty"`
133+ // List of configured LSPs
134+ LSP map[string]LSPConfig `json:"lsp,omitempty"`
136+ // Miscellaneous options
137+ Options Options `json:"options"`
139+ // Used to add models that are not already in the repository
140+ Models map[provider.InferenceProvider][]provider.Model `json:"models,omitempty"`
144+ instance *Config // The single instance of the Singleton
146+ once sync.Once // Ensures the initialization happens only once
149+func loadConfig(cwd string) (*Config, error) {
150+ // First read the global config file
151+ cfgPath := ConfigPath()
153+ cfg := defaultConfigBasedOnEnv()
155+ var globalCfg *Config
156+ if _, err := os.Stat(cfgPath); err != nil && !os.IsNotExist(err) {
157+ // some other error occurred while checking the file
159+ } else if err == nil {
160+ // config file exists, read it
161+ file, err := os.ReadFile(cfgPath)
165+ globalCfg = &Config{}
166+ if err := json.Unmarshal(file, globalCfg); err != nil {
170+ // config file does not exist, create a new one
171+ globalCfg = &Config{}
174+ var localConfig *Config
175+ // Global config loaded, now read the local config file
176+ localConfigPath := filepath.Join(cwd, "crush.json")
177+ if _, err := os.Stat(localConfigPath); err != nil && !os.IsNotExist(err) {
178+ // some other error occurred while checking the file
180+ } else if err == nil {
181+ // local config file exists, read it
182+ file, err := os.ReadFile(localConfigPath)
186+ localConfig = &Config{}
187+ if err := json.Unmarshal(file, localConfig); err != nil {
193+ cfg.Options = mergeOptions(cfg.Options, globalCfg.Options)
194+ cfg.Options = mergeOptions(cfg.Options, localConfig.Options)
196+ mergeProviderConfigs(cfg, globalCfg, localConfig)
200+func InitConfig(workingDir string) *Config {
203+ cfg, err := loadConfig(cwd)
205+ // TODO: Handle this better
206+ panic("Failed to load config: " + err.Error())
214+func GetConfig() *Config {
215+ if instance == nil {
216+ // TODO: Handle this better
217+ panic("Config not initialized. Call InitConfig first.")
222+func mergeProviderConfig(p provider.InferenceProvider, base, other ProviderConfig) ProviderConfig {
223+ if other.APIKey != "" {
224+ base.APIKey = other.APIKey
226+ // Only change these options if the provider is not a known provider
227+ if !slices.Contains(provider.KnownProviders(), p) {
228+ if other.BaseURL != "" {
229+ base.BaseURL = other.BaseURL
231+ if other.ProviderType != "" {
232+ base.ProviderType = other.ProviderType
234+ if len(base.ExtraHeaders) > 0 {
235+ if base.ExtraHeaders == nil {
236+ base.ExtraHeaders = make(map[string]string)
238+ maps.Copy(base.ExtraHeaders, other.ExtraHeaders)
240+ if len(other.ExtraParams) > 0 {
241+ if base.ExtraParams == nil {
242+ base.ExtraParams = make(map[string]string)
244+ maps.Copy(base.ExtraParams, other.ExtraParams)
249+ base.Disabled = other.Disabled
255+func validateProvider(p provider.InferenceProvider, providerConfig ProviderConfig) error {
256+ if !slices.Contains(provider.KnownProviders(), p) {
257+ if providerConfig.ProviderType != provider.TypeOpenAI {
258+ return errors.New("invalid provider type: " + string(providerConfig.ProviderType))
260+ if providerConfig.BaseURL == "" {
261+ return errors.New("base URL must be set for custom providers")
263+ if providerConfig.APIKey == "" {
264+ return errors.New("API key must be set for custom providers")
270+func mergeOptions(base, other Options) Options {
273+ if len(other.ContextPaths) > 0 {
274+ base.ContextPaths = append(base.ContextPaths, other.ContextPaths...)
277+ if other.TUI.CompactMode {
278+ result.TUI.CompactMode = other.TUI.CompactMode
282+ result.Debug = other.Debug
286+ result.DebugLSP = other.DebugLSP
289+ if other.DisableAutoSummarize {
290+ result.DisableAutoSummarize = other.DisableAutoSummarize
293+ if other.DataDirectory != "" {
294+ result.DataDirectory = other.DataDirectory
300+func mergeProviderConfigs(base, global, local *Config) {
302+ for providerName, globalProvider := range global.Providers {
303+ if _, ok := base.Providers[providerName]; !ok {
304+ base.Providers[providerName] = globalProvider
306+ base.Providers[providerName] = mergeProviderConfig(providerName, base.Providers[providerName], globalProvider)
311+ for providerName, localProvider := range local.Providers {
312+ if _, ok := base.Providers[providerName]; !ok {
313+ base.Providers[providerName] = localProvider
315+ base.Providers[providerName] = mergeProviderConfig(providerName, base.Providers[providerName], localProvider)
320+ finalProviders := make(map[provider.InferenceProvider]ProviderConfig)
321+ for providerName, providerConfig := range base.Providers {
322+ err := validateProvider(providerName, providerConfig)
324+ logging.Warn("Skipping provider", "name", providerName, "error", err)
326+ finalProviders[providerName] = providerConfig
328+ base.Providers = finalProviders
331+func providerDefaultConfig(providerName provider.InferenceProvider) ProviderConfig {
332+ switch providerName {
333+ case provider.InferenceProviderAnthropic:
334+ return ProviderConfig{
335+ ProviderType: provider.TypeAnthropic,
337+ case provider.InferenceProviderOpenAI:
338+ return ProviderConfig{
339+ ProviderType: provider.TypeOpenAI,
341+ case provider.InferenceProviderGemini:
342+ return ProviderConfig{
343+ ProviderType: provider.TypeGemini,
345+ case provider.InferenceProviderBedrock:
346+ return ProviderConfig{
347+ ProviderType: provider.TypeBedrock,
349+ case provider.InferenceProviderAzure:
350+ return ProviderConfig{
351+ ProviderType: provider.TypeAzure,
353+ case provider.InferenceProviderOpenRouter:
354+ return ProviderConfig{
355+ ProviderType: provider.TypeOpenAI,
356+ BaseURL: "https://openrouter.ai/api/v1",
357+ ExtraHeaders: map[string]string{
358+ "HTTP-Referer": "crush.charm.land",
362+ case provider.InferenceProviderXAI:
363+ return ProviderConfig{
364+ ProviderType: provider.TypeXAI,
365+ BaseURL: "https://api.x.ai/v1",
367+ case provider.InferenceProviderVertexAI:
368+ return ProviderConfig{
369+ ProviderType: provider.TypeVertexAI,
372+ return ProviderConfig{
373+ ProviderType: provider.TypeOpenAI,
378+func defaultConfigBasedOnEnv() *Config {
381+ DataDirectory: defaultDataDirectory,
383+ Providers: make(map[provider.InferenceProvider]ProviderConfig),
386+ providers := Providers()
388+ for _, p := range providers {
389+ if strings.HasPrefix(p.APIKey, "$") {
390+ envVar := strings.TrimPrefix(p.APIKey, "$")
391+ if apiKey := os.Getenv(envVar); apiKey != "" {
392+ providerConfig := providerDefaultConfig(p.ID)
393+ providerConfig.APIKey = apiKey
394+ providerConfig.DefaultModel = p.DefaultModelID
395+ cfg.Providers[p.ID] = providerConfig
399+ // TODO: support local models
401+ if useVertexAI := os.Getenv("GOOGLE_GENAI_USE_VERTEXAI"); useVertexAI == "true" {
402+ providerConfig := providerDefaultConfig(provider.InferenceProviderVertexAI)
403+ providerConfig.ExtraParams = map[string]string{
404+ "project": os.Getenv("GOOGLE_CLOUD_PROJECT"),
405+ "location": os.Getenv("GOOGLE_CLOUD_LOCATION"),
407+ cfg.Providers[provider.InferenceProviderVertexAI] = providerConfig
410+ if hasAWSCredentials() {
411+ providerConfig := providerDefaultConfig(provider.InferenceProviderBedrock)
412+ cfg.Providers[provider.InferenceProviderBedrock] = providerConfig
417+func hasAWSCredentials() bool {
418+ if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
422+ if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
426+ if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" {
430+ if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
431+ os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
438+func WorkingDirectory() string {