config.go

  1package backend
  2
  3import (
  4	"context"
  5	"errors"
  6	"fmt"
  7
  8	"github.com/charmbracelet/crush/internal/agent"
  9	mcptools "github.com/charmbracelet/crush/internal/agent/tools/mcp"
 10	"github.com/charmbracelet/crush/internal/commands"
 11	"github.com/charmbracelet/crush/internal/config"
 12	"github.com/charmbracelet/crush/internal/oauth"
 13)
 14
 15// MCPResourceContents holds the contents of an MCP resource returned
 16// by the backend.
 17type MCPResourceContents struct {
 18	URI      string `json:"uri"`
 19	MIMEType string `json:"mime_type,omitempty"`
 20	Text     string `json:"text,omitempty"`
 21	Blob     []byte `json:"blob,omitempty"`
 22}
 23
 24// SetConfigField sets a key/value pair in the config file for the
 25// given scope.
 26func (b *Backend) SetConfigField(workspaceID string, scope config.Scope, key string, value any) error {
 27	ws, err := b.GetWorkspace(workspaceID)
 28	if err != nil {
 29		return err
 30	}
 31	return ws.Cfg.SetConfigField(scope, key, value)
 32}
 33
 34// RemoveConfigField removes a key from the config file for the given
 35// scope.
 36func (b *Backend) RemoveConfigField(workspaceID string, scope config.Scope, key string) error {
 37	ws, err := b.GetWorkspace(workspaceID)
 38	if err != nil {
 39		return err
 40	}
 41	return ws.Cfg.RemoveConfigField(scope, key)
 42}
 43
 44// UpdatePreferredModel updates the preferred model for the given type
 45// and persists it to the config file at the given scope.
 46func (b *Backend) UpdatePreferredModel(workspaceID string, scope config.Scope, modelType config.SelectedModelType, model config.SelectedModel) error {
 47	ws, err := b.GetWorkspace(workspaceID)
 48	if err != nil {
 49		return err
 50	}
 51	return ws.Cfg.UpdatePreferredModel(scope, modelType, model)
 52}
 53
 54// SetCompactMode sets the compact mode setting and persists it.
 55func (b *Backend) SetCompactMode(workspaceID string, scope config.Scope, enabled bool) error {
 56	ws, err := b.GetWorkspace(workspaceID)
 57	if err != nil {
 58		return err
 59	}
 60	return ws.Cfg.SetCompactMode(scope, enabled)
 61}
 62
 63// SetProviderAPIKey sets the API key for a provider and persists it.
 64func (b *Backend) SetProviderAPIKey(workspaceID string, scope config.Scope, providerID string, apiKey any) error {
 65	ws, err := b.GetWorkspace(workspaceID)
 66	if err != nil {
 67		return err
 68	}
 69	return ws.Cfg.SetProviderAPIKey(scope, providerID, apiKey)
 70}
 71
 72// ImportCopilot attempts to import a GitHub Copilot token from disk.
 73func (b *Backend) ImportCopilot(workspaceID string) (*oauth.Token, bool, error) {
 74	ws, err := b.GetWorkspace(workspaceID)
 75	if err != nil {
 76		return nil, false, err
 77	}
 78	token, ok := ws.Cfg.ImportCopilot()
 79	return token, ok, nil
 80}
 81
 82// RefreshOAuthToken refreshes the OAuth token for a provider.
 83func (b *Backend) RefreshOAuthToken(ctx context.Context, workspaceID string, scope config.Scope, providerID string) error {
 84	ws, err := b.GetWorkspace(workspaceID)
 85	if err != nil {
 86		return err
 87	}
 88	return ws.Cfg.RefreshOAuthToken(ctx, scope, providerID)
 89}
 90
 91// ProjectNeedsInitialization checks whether the project in this
 92// workspace needs initialization.
 93func (b *Backend) ProjectNeedsInitialization(workspaceID string) (bool, error) {
 94	ws, err := b.GetWorkspace(workspaceID)
 95	if err != nil {
 96		return false, err
 97	}
 98	return config.ProjectNeedsInitialization(ws.Cfg)
 99}
100
101// MarkProjectInitialized marks the project as initialized.
102func (b *Backend) MarkProjectInitialized(workspaceID string) error {
103	ws, err := b.GetWorkspace(workspaceID)
104	if err != nil {
105		return err
106	}
107	return config.MarkProjectInitialized(ws.Cfg)
108}
109
110// InitializePrompt builds the initialization prompt for the workspace.
111func (b *Backend) InitializePrompt(workspaceID string) (string, error) {
112	ws, err := b.GetWorkspace(workspaceID)
113	if err != nil {
114		return "", err
115	}
116	return agent.InitializePrompt(ws.Cfg)
117}
118
119// EnableDockerMCP validates Docker MCP availability, stages the
120// configuration, starts the MCP client, and persists the config.
121func (b *Backend) EnableDockerMCP(ctx context.Context, workspaceID string) error {
122	ws, err := b.GetWorkspace(workspaceID)
123	if err != nil {
124		return err
125	}
126
127	mcpConfig, err := ws.Cfg.PrepareDockerMCPConfig()
128	if err != nil {
129		return err
130	}
131
132	if err := mcptools.InitializeSingle(ctx, config.DockerMCPName, ws.Cfg); err != nil {
133		disableErr := mcptools.DisableSingle(ws.Cfg, config.DockerMCPName)
134		delete(ws.Cfg.Config().MCP, config.DockerMCPName)
135		return fmt.Errorf("failed to start docker MCP: %w", errors.Join(err, disableErr))
136	}
137
138	if err := ws.Cfg.PersistDockerMCPConfig(mcpConfig); err != nil {
139		disableErr := mcptools.DisableSingle(ws.Cfg, config.DockerMCPName)
140		delete(ws.Cfg.Config().MCP, config.DockerMCPName)
141		return fmt.Errorf("docker MCP started but failed to persist configuration: %w", errors.Join(err, disableErr))
142	}
143
144	return nil
145}
146
147// DisableDockerMCP closes the Docker MCP client, removes the
148// configuration, and persists the change.
149func (b *Backend) DisableDockerMCP(workspaceID string) error {
150	ws, err := b.GetWorkspace(workspaceID)
151	if err != nil {
152		return err
153	}
154
155	if err := mcptools.DisableSingle(ws.Cfg, config.DockerMCPName); err != nil {
156		return fmt.Errorf("failed to disable docker MCP: %w", err)
157	}
158
159	if err := ws.Cfg.DisableDockerMCP(); err != nil {
160		return err
161	}
162
163	return nil
164}
165
166// RefreshMCPTools refreshes the tools for a named MCP server.
167func (b *Backend) RefreshMCPTools(ctx context.Context, workspaceID, name string) error {
168	ws, err := b.GetWorkspace(workspaceID)
169	if err != nil {
170		return err
171	}
172	mcptools.RefreshTools(ctx, ws.Cfg, name)
173	return nil
174}
175
176// ReadMCPResource reads a resource from a named MCP server.
177func (b *Backend) ReadMCPResource(ctx context.Context, workspaceID, name, uri string) ([]MCPResourceContents, error) {
178	ws, err := b.GetWorkspace(workspaceID)
179	if err != nil {
180		return nil, err
181	}
182	contents, err := mcptools.ReadResource(ctx, ws.Cfg, name, uri)
183	if err != nil {
184		return nil, err
185	}
186	result := make([]MCPResourceContents, len(contents))
187	for i, c := range contents {
188		result[i] = MCPResourceContents{
189			URI:      c.URI,
190			MIMEType: c.MIMEType,
191			Text:     c.Text,
192			Blob:     c.Blob,
193		}
194	}
195	return result, nil
196}
197
198// GetMCPPrompt retrieves a prompt from a named MCP server.
199func (b *Backend) GetMCPPrompt(workspaceID, clientID, promptID string, args map[string]string) (string, error) {
200	ws, err := b.GetWorkspace(workspaceID)
201	if err != nil {
202		return "", err
203	}
204	return commands.GetMCPPrompt(ws.Cfg, clientID, promptID, args)
205}
206
207// GetWorkingDir returns the working directory for a workspace.
208func (b *Backend) GetWorkingDir(workspaceID string) (string, error) {
209	ws, err := b.GetWorkspace(workspaceID)
210	if err != nil {
211		return "", err
212	}
213	return ws.Cfg.WorkingDir(), nil
214}