config.go

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