config.go

  1package server
  2
  3import (
  4	"encoding/json"
  5	"net/http"
  6
  7	"github.com/charmbracelet/crush/internal/proto"
  8)
  9
 10// handlePostWorkspaceConfigSet sets a configuration field.
 11//
 12//	@Summary		Set a config field
 13//	@Tags			config
 14//	@Accept			json
 15//	@Param			id		path	string					true	"Workspace ID"
 16//	@Param			request	body	proto.ConfigSetRequest	true	"Config set request"
 17//	@Success		200
 18//	@Failure		400	{object}	proto.Error
 19//	@Failure		404	{object}	proto.Error
 20//	@Failure		500	{object}	proto.Error
 21//	@Router			/workspaces/{id}/config/set [post]
 22func (c *controllerV1) handlePostWorkspaceConfigSet(w http.ResponseWriter, r *http.Request) {
 23	id := r.PathValue("id")
 24
 25	var req proto.ConfigSetRequest
 26	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 27		c.server.logError(r, "Failed to decode request", "error", err)
 28		jsonError(w, http.StatusBadRequest, "failed to decode request")
 29		return
 30	}
 31
 32	if err := c.backend.SetConfigField(id, req.Scope, req.Key, req.Value); err != nil {
 33		c.handleError(w, r, err)
 34		return
 35	}
 36	w.WriteHeader(http.StatusOK)
 37}
 38
 39// handlePostWorkspaceConfigRemove removes a configuration field.
 40//
 41//	@Summary		Remove a config field
 42//	@Tags			config
 43//	@Accept			json
 44//	@Param			id		path	string						true	"Workspace ID"
 45//	@Param			request	body	proto.ConfigRemoveRequest	true	"Config remove request"
 46//	@Success		200
 47//	@Failure		400	{object}	proto.Error
 48//	@Failure		404	{object}	proto.Error
 49//	@Failure		500	{object}	proto.Error
 50//	@Router			/workspaces/{id}/config/remove [post]
 51func (c *controllerV1) handlePostWorkspaceConfigRemove(w http.ResponseWriter, r *http.Request) {
 52	id := r.PathValue("id")
 53
 54	var req proto.ConfigRemoveRequest
 55	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 56		c.server.logError(r, "Failed to decode request", "error", err)
 57		jsonError(w, http.StatusBadRequest, "failed to decode request")
 58		return
 59	}
 60
 61	if err := c.backend.RemoveConfigField(id, req.Scope, req.Key); err != nil {
 62		c.handleError(w, r, err)
 63		return
 64	}
 65	w.WriteHeader(http.StatusOK)
 66}
 67
 68// handlePostWorkspaceConfigModel updates the preferred model.
 69//
 70//	@Summary		Set the preferred model
 71//	@Tags			config
 72//	@Accept			json
 73//	@Param			id		path	string						true	"Workspace ID"
 74//	@Param			request	body	proto.ConfigModelRequest	true	"Config model request"
 75//	@Success		200
 76//	@Failure		400	{object}	proto.Error
 77//	@Failure		404	{object}	proto.Error
 78//	@Failure		500	{object}	proto.Error
 79//	@Router			/workspaces/{id}/config/model [post]
 80func (c *controllerV1) handlePostWorkspaceConfigModel(w http.ResponseWriter, r *http.Request) {
 81	id := r.PathValue("id")
 82
 83	var req proto.ConfigModelRequest
 84	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 85		c.server.logError(r, "Failed to decode request", "error", err)
 86		jsonError(w, http.StatusBadRequest, "failed to decode request")
 87		return
 88	}
 89
 90	if err := c.backend.UpdatePreferredModel(id, req.Scope, req.ModelType, req.Model); err != nil {
 91		c.handleError(w, r, err)
 92		return
 93	}
 94	w.WriteHeader(http.StatusOK)
 95}
 96
 97// handlePostWorkspaceConfigCompact sets compact mode.
 98//
 99//	@Summary		Set compact mode
100//	@Tags			config
101//	@Accept			json
102//	@Param			id		path	string						true	"Workspace ID"
103//	@Param			request	body	proto.ConfigCompactRequest	true	"Config compact request"
104//	@Success		200
105//	@Failure		400	{object}	proto.Error
106//	@Failure		404	{object}	proto.Error
107//	@Failure		500	{object}	proto.Error
108//	@Router			/workspaces/{id}/config/compact [post]
109func (c *controllerV1) handlePostWorkspaceConfigCompact(w http.ResponseWriter, r *http.Request) {
110	id := r.PathValue("id")
111
112	var req proto.ConfigCompactRequest
113	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
114		c.server.logError(r, "Failed to decode request", "error", err)
115		jsonError(w, http.StatusBadRequest, "failed to decode request")
116		return
117	}
118
119	if err := c.backend.SetCompactMode(id, req.Scope, req.Enabled); err != nil {
120		c.handleError(w, r, err)
121		return
122	}
123	w.WriteHeader(http.StatusOK)
124}
125
126// handlePostWorkspaceConfigProviderKey sets a provider API key.
127//
128//	@Summary		Set provider API key
129//	@Tags			config
130//	@Accept			json
131//	@Param			id		path	string							true	"Workspace ID"
132//	@Param			request	body	proto.ConfigProviderKeyRequest	true	"Config provider key request"
133//	@Success		200
134//	@Failure		400	{object}	proto.Error
135//	@Failure		404	{object}	proto.Error
136//	@Failure		500	{object}	proto.Error
137//	@Router			/workspaces/{id}/config/provider-key [post]
138func (c *controllerV1) handlePostWorkspaceConfigProviderKey(w http.ResponseWriter, r *http.Request) {
139	id := r.PathValue("id")
140
141	var req proto.ConfigProviderKeyRequest
142	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
143		c.server.logError(r, "Failed to decode request", "error", err)
144		jsonError(w, http.StatusBadRequest, "failed to decode request")
145		return
146	}
147
148	if err := c.backend.SetProviderAPIKey(id, req.Scope, req.ProviderID, req.APIKey); err != nil {
149		c.handleError(w, r, err)
150		return
151	}
152	w.WriteHeader(http.StatusOK)
153}
154
155// handlePostWorkspaceConfigImportCopilot imports Copilot credentials.
156//
157//	@Summary		Import Copilot credentials
158//	@Tags			config
159//	@Produce		json
160//	@Param			id	path		string						true	"Workspace ID"
161//	@Success		200	{object}	proto.ImportCopilotResponse
162//	@Failure		404	{object}	proto.Error
163//	@Failure		500	{object}	proto.Error
164//	@Router			/workspaces/{id}/config/import-copilot [post]
165func (c *controllerV1) handlePostWorkspaceConfigImportCopilot(w http.ResponseWriter, r *http.Request) {
166	id := r.PathValue("id")
167	token, ok, err := c.backend.ImportCopilot(id)
168	if err != nil {
169		c.handleError(w, r, err)
170		return
171	}
172	jsonEncode(w, proto.ImportCopilotResponse{Token: token, Success: ok})
173}
174
175// handlePostWorkspaceConfigRefreshOAuth refreshes an OAuth token for a provider.
176//
177//	@Summary		Refresh OAuth token
178//	@Tags			config
179//	@Accept			json
180//	@Param			id		path	string							true	"Workspace ID"
181//	@Param			request	body	proto.ConfigRefreshOAuthRequest	true	"Refresh OAuth request"
182//	@Success		200
183//	@Failure		400	{object}	proto.Error
184//	@Failure		404	{object}	proto.Error
185//	@Failure		500	{object}	proto.Error
186//	@Router			/workspaces/{id}/config/refresh-oauth [post]
187func (c *controllerV1) handlePostWorkspaceConfigRefreshOAuth(w http.ResponseWriter, r *http.Request) {
188	id := r.PathValue("id")
189
190	var req proto.ConfigRefreshOAuthRequest
191	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
192		c.server.logError(r, "Failed to decode request", "error", err)
193		jsonError(w, http.StatusBadRequest, "failed to decode request")
194		return
195	}
196
197	if err := c.backend.RefreshOAuthToken(r.Context(), id, req.Scope, req.ProviderID); err != nil {
198		c.handleError(w, r, err)
199		return
200	}
201	w.WriteHeader(http.StatusOK)
202}
203
204// handleGetWorkspaceProjectNeedsInit reports whether a project needs initialization.
205//
206//	@Summary		Check if project needs initialization
207//	@Tags			project
208//	@Produce		json
209//	@Param			id	path		string							true	"Workspace ID"
210//	@Success		200	{object}	proto.ProjectNeedsInitResponse
211//	@Failure		404	{object}	proto.Error
212//	@Failure		500	{object}	proto.Error
213//	@Router			/workspaces/{id}/project/needs-init [get]
214func (c *controllerV1) handleGetWorkspaceProjectNeedsInit(w http.ResponseWriter, r *http.Request) {
215	id := r.PathValue("id")
216	needs, err := c.backend.ProjectNeedsInitialization(id)
217	if err != nil {
218		c.handleError(w, r, err)
219		return
220	}
221	jsonEncode(w, proto.ProjectNeedsInitResponse{NeedsInit: needs})
222}
223
224// handlePostWorkspaceProjectInit marks the project as initialized.
225//
226//	@Summary		Mark project as initialized
227//	@Tags			project
228//	@Param			id	path	string	true	"Workspace ID"
229//	@Success		200
230//	@Failure		404	{object}	proto.Error
231//	@Failure		500	{object}	proto.Error
232//	@Router			/workspaces/{id}/project/init [post]
233func (c *controllerV1) handlePostWorkspaceProjectInit(w http.ResponseWriter, r *http.Request) {
234	id := r.PathValue("id")
235	if err := c.backend.MarkProjectInitialized(id); err != nil {
236		c.handleError(w, r, err)
237		return
238	}
239	w.WriteHeader(http.StatusOK)
240}
241
242// handleGetWorkspaceProjectInitPrompt returns the project initialization prompt.
243//
244//	@Summary		Get project initialization prompt
245//	@Tags			project
246//	@Produce		json
247//	@Param			id	path		string							true	"Workspace ID"
248//	@Success		200	{object}	proto.ProjectInitPromptResponse
249//	@Failure		404	{object}	proto.Error
250//	@Failure		500	{object}	proto.Error
251//	@Router			/workspaces/{id}/project/init-prompt [get]
252func (c *controllerV1) handleGetWorkspaceProjectInitPrompt(w http.ResponseWriter, r *http.Request) {
253	id := r.PathValue("id")
254	prompt, err := c.backend.InitializePrompt(id)
255	if err != nil {
256		c.handleError(w, r, err)
257		return
258	}
259	jsonEncode(w, proto.ProjectInitPromptResponse{Prompt: prompt})
260}
261
262// handlePostWorkspaceMCPEnableDocker enables the Docker MCP server.
263//
264//	@Summary		Enable Docker MCP
265//	@Tags			mcp
266//	@Param			id	path	string	true	"Workspace ID"
267//	@Success		200
268//	@Failure		404	{object}	proto.Error
269//	@Failure		500	{object}	proto.Error
270//	@Router			/workspaces/{id}/mcp/docker/enable [post]
271func (c *controllerV1) handlePostWorkspaceMCPEnableDocker(w http.ResponseWriter, r *http.Request) {
272	id := r.PathValue("id")
273	if err := c.backend.EnableDockerMCP(r.Context(), id); err != nil {
274		c.handleError(w, r, err)
275		return
276	}
277	w.WriteHeader(http.StatusOK)
278}
279
280// handlePostWorkspaceMCPDisableDocker disables the Docker MCP server.
281//
282//	@Summary		Disable Docker MCP
283//	@Tags			mcp
284//	@Param			id	path	string	true	"Workspace ID"
285//	@Success		200
286//	@Failure		404	{object}	proto.Error
287//	@Failure		500	{object}	proto.Error
288//	@Router			/workspaces/{id}/mcp/docker/disable [post]
289func (c *controllerV1) handlePostWorkspaceMCPDisableDocker(w http.ResponseWriter, r *http.Request) {
290	id := r.PathValue("id")
291	if err := c.backend.DisableDockerMCP(id); err != nil {
292		c.handleError(w, r, err)
293		return
294	}
295	w.WriteHeader(http.StatusOK)
296}
297
298// handlePostWorkspaceMCPRefreshTools refreshes tools for a named MCP server.
299//
300//	@Summary		Refresh MCP tools
301//	@Tags			mcp
302//	@Accept			json
303//	@Param			id		path	string					true	"Workspace ID"
304//	@Param			request	body	proto.MCPNameRequest	true	"MCP name request"
305//	@Success		200
306//	@Failure		400	{object}	proto.Error
307//	@Failure		404	{object}	proto.Error
308//	@Failure		500	{object}	proto.Error
309//	@Router			/workspaces/{id}/mcp/refresh-tools [post]
310func (c *controllerV1) handlePostWorkspaceMCPRefreshTools(w http.ResponseWriter, r *http.Request) {
311	id := r.PathValue("id")
312
313	var req proto.MCPNameRequest
314	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
315		c.server.logError(r, "Failed to decode request", "error", err)
316		jsonError(w, http.StatusBadRequest, "failed to decode request")
317		return
318	}
319
320	if err := c.backend.RefreshMCPTools(r.Context(), id, req.Name); err != nil {
321		c.handleError(w, r, err)
322		return
323	}
324	w.WriteHeader(http.StatusOK)
325}
326
327// handlePostWorkspaceMCPReadResource reads a resource from an MCP server.
328//
329//	@Summary		Read MCP resource
330//	@Tags			mcp
331//	@Accept			json
332//	@Produce		json
333//	@Param			id		path		string						true	"Workspace ID"
334//	@Param			request	body		proto.MCPReadResourceRequest	true	"MCP read resource request"
335//	@Success		200		{object}	object
336//	@Failure		400		{object}	proto.Error
337//	@Failure		404		{object}	proto.Error
338//	@Failure		500		{object}	proto.Error
339//	@Router			/workspaces/{id}/mcp/read-resource [post]
340func (c *controllerV1) handlePostWorkspaceMCPReadResource(w http.ResponseWriter, r *http.Request) {
341	id := r.PathValue("id")
342
343	var req proto.MCPReadResourceRequest
344	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
345		c.server.logError(r, "Failed to decode request", "error", err)
346		jsonError(w, http.StatusBadRequest, "failed to decode request")
347		return
348	}
349
350	contents, err := c.backend.ReadMCPResource(r.Context(), id, req.Name, req.URI)
351	if err != nil {
352		c.handleError(w, r, err)
353		return
354	}
355	jsonEncode(w, contents)
356}
357
358// handlePostWorkspaceMCPGetPrompt retrieves a prompt from an MCP server.
359//
360//	@Summary		Get MCP prompt
361//	@Tags			mcp
362//	@Accept			json
363//	@Produce		json
364//	@Param			id		path		string						true	"Workspace ID"
365//	@Param			request	body		proto.MCPGetPromptRequest	true	"MCP get prompt request"
366//	@Success		200		{object}	proto.MCPGetPromptResponse
367//	@Failure		400		{object}	proto.Error
368//	@Failure		404		{object}	proto.Error
369//	@Failure		500		{object}	proto.Error
370//	@Router			/workspaces/{id}/mcp/get-prompt [post]
371func (c *controllerV1) handlePostWorkspaceMCPGetPrompt(w http.ResponseWriter, r *http.Request) {
372	id := r.PathValue("id")
373
374	var req proto.MCPGetPromptRequest
375	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
376		c.server.logError(r, "Failed to decode request", "error", err)
377		jsonError(w, http.StatusBadRequest, "failed to decode request")
378		return
379	}
380
381	prompt, err := c.backend.GetMCPPrompt(id, req.ClientID, req.PromptID, req.Args)
382	if err != nil {
383		c.handleError(w, r, err)
384		return
385	}
386	jsonEncode(w, proto.MCPGetPromptResponse{Prompt: prompt})
387}
388
389// handleGetWorkspaceMCPStates returns the state of all MCP clients.
390//
391//	@Summary		Get MCP client states
392//	@Tags			mcp
393//	@Produce		json
394//	@Param			id	path		string						true	"Workspace ID"
395//	@Success		200	{object}	map[string]proto.MCPClientInfo
396//	@Failure		404	{object}	proto.Error
397//	@Failure		500	{object}	proto.Error
398//	@Router			/workspaces/{id}/mcp/states [get]
399func (c *controllerV1) handleGetWorkspaceMCPStates(w http.ResponseWriter, r *http.Request) {
400	id := r.PathValue("id")
401	states := c.backend.MCPGetStates(id)
402	result := make(map[string]proto.MCPClientInfo, len(states))
403	for k, v := range states {
404		result[k] = proto.MCPClientInfo{
405			Name:          v.Name,
406			State:         proto.MCPState(v.State),
407			Error:         v.Error,
408			ToolCount:     v.Counts.Tools,
409			PromptCount:   v.Counts.Prompts,
410			ResourceCount: v.Counts.Resources,
411			ConnectedAt:   v.ConnectedAt,
412		}
413	}
414	jsonEncode(w, result)
415}
416
417// handlePostWorkspaceMCPRefreshPrompts refreshes prompts for a named MCP server.
418//
419//	@Summary		Refresh MCP prompts
420//	@Tags			mcp
421//	@Accept			json
422//	@Param			id		path	string					true	"Workspace ID"
423//	@Param			request	body	proto.MCPNameRequest	true	"MCP name request"
424//	@Success		200
425//	@Failure		400	{object}	proto.Error
426//	@Failure		404	{object}	proto.Error
427//	@Failure		500	{object}	proto.Error
428//	@Router			/workspaces/{id}/mcp/refresh-prompts [post]
429func (c *controllerV1) handlePostWorkspaceMCPRefreshPrompts(w http.ResponseWriter, r *http.Request) {
430	id := r.PathValue("id")
431
432	var req proto.MCPNameRequest
433	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
434		c.server.logError(r, "Failed to decode request", "error", err)
435		jsonError(w, http.StatusBadRequest, "failed to decode request")
436		return
437	}
438
439	c.backend.MCPRefreshPrompts(r.Context(), id, req.Name)
440	w.WriteHeader(http.StatusOK)
441}
442
443// handlePostWorkspaceMCPRefreshResources refreshes resources for a named MCP server.
444//
445//	@Summary		Refresh MCP resources
446//	@Tags			mcp
447//	@Accept			json
448//	@Param			id		path	string					true	"Workspace ID"
449//	@Param			request	body	proto.MCPNameRequest	true	"MCP name request"
450//	@Success		200
451//	@Failure		400	{object}	proto.Error
452//	@Failure		404	{object}	proto.Error
453//	@Failure		500	{object}	proto.Error
454//	@Router			/workspaces/{id}/mcp/refresh-resources [post]
455func (c *controllerV1) handlePostWorkspaceMCPRefreshResources(w http.ResponseWriter, r *http.Request) {
456	id := r.PathValue("id")
457
458	var req proto.MCPNameRequest
459	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
460		c.server.logError(r, "Failed to decode request", "error", err)
461		jsonError(w, http.StatusBadRequest, "failed to decode request")
462		return
463	}
464
465	c.backend.MCPRefreshResources(r.Context(), id, req.Name)
466	w.WriteHeader(http.StatusOK)
467}