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	apiKey, err := req.DecodeAPIKey()
149	if err != nil {
150		c.server.logError(r, "Failed to decode api key", "error", err, "kind", req.Kind)
151		jsonError(w, http.StatusBadRequest, err.Error())
152		return
153	}
154
155	if err := c.backend.SetProviderAPIKey(id, req.Scope, req.ProviderID, apiKey); err != nil {
156		c.handleError(w, r, err)
157		return
158	}
159	w.WriteHeader(http.StatusOK)
160}
161
162// handlePostWorkspaceConfigImportCopilot imports Copilot credentials.
163//
164//	@Summary		Import Copilot credentials
165//	@Tags			config
166//	@Produce		json
167//	@Param			id	path		string						true	"Workspace ID"
168//	@Success		200	{object}	proto.ImportCopilotResponse
169//	@Failure		404	{object}	proto.Error
170//	@Failure		500	{object}	proto.Error
171//	@Router			/workspaces/{id}/config/import-copilot [post]
172func (c *controllerV1) handlePostWorkspaceConfigImportCopilot(w http.ResponseWriter, r *http.Request) {
173	id := r.PathValue("id")
174	token, ok, err := c.backend.ImportCopilot(id)
175	if err != nil {
176		c.handleError(w, r, err)
177		return
178	}
179	jsonEncode(w, proto.ImportCopilotResponse{Token: token, Success: ok})
180}
181
182// handlePostWorkspaceConfigRefreshOAuth refreshes an OAuth token for a provider.
183//
184//	@Summary		Refresh OAuth token
185//	@Tags			config
186//	@Accept			json
187//	@Param			id		path	string							true	"Workspace ID"
188//	@Param			request	body	proto.ConfigRefreshOAuthRequest	true	"Refresh OAuth request"
189//	@Success		200
190//	@Failure		400	{object}	proto.Error
191//	@Failure		404	{object}	proto.Error
192//	@Failure		500	{object}	proto.Error
193//	@Router			/workspaces/{id}/config/refresh-oauth [post]
194func (c *controllerV1) handlePostWorkspaceConfigRefreshOAuth(w http.ResponseWriter, r *http.Request) {
195	id := r.PathValue("id")
196
197	var req proto.ConfigRefreshOAuthRequest
198	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
199		c.server.logError(r, "Failed to decode request", "error", err)
200		jsonError(w, http.StatusBadRequest, "failed to decode request")
201		return
202	}
203
204	if err := c.backend.RefreshOAuthToken(r.Context(), id, req.Scope, req.ProviderID); err != nil {
205		c.handleError(w, r, err)
206		return
207	}
208	w.WriteHeader(http.StatusOK)
209}
210
211// handleGetWorkspaceProjectNeedsInit reports whether a project needs initialization.
212//
213//	@Summary		Check if project needs initialization
214//	@Tags			project
215//	@Produce		json
216//	@Param			id	path		string							true	"Workspace ID"
217//	@Success		200	{object}	proto.ProjectNeedsInitResponse
218//	@Failure		404	{object}	proto.Error
219//	@Failure		500	{object}	proto.Error
220//	@Router			/workspaces/{id}/project/needs-init [get]
221func (c *controllerV1) handleGetWorkspaceProjectNeedsInit(w http.ResponseWriter, r *http.Request) {
222	id := r.PathValue("id")
223	needs, err := c.backend.ProjectNeedsInitialization(id)
224	if err != nil {
225		c.handleError(w, r, err)
226		return
227	}
228	jsonEncode(w, proto.ProjectNeedsInitResponse{NeedsInit: needs})
229}
230
231// handlePostWorkspaceProjectInit marks the project as initialized.
232//
233//	@Summary		Mark project as initialized
234//	@Tags			project
235//	@Param			id	path	string	true	"Workspace ID"
236//	@Success		200
237//	@Failure		404	{object}	proto.Error
238//	@Failure		500	{object}	proto.Error
239//	@Router			/workspaces/{id}/project/init [post]
240func (c *controllerV1) handlePostWorkspaceProjectInit(w http.ResponseWriter, r *http.Request) {
241	id := r.PathValue("id")
242	if err := c.backend.MarkProjectInitialized(id); err != nil {
243		c.handleError(w, r, err)
244		return
245	}
246	w.WriteHeader(http.StatusOK)
247}
248
249// handleGetWorkspaceProjectInitPrompt returns the project initialization prompt.
250//
251//	@Summary		Get project initialization prompt
252//	@Tags			project
253//	@Produce		json
254//	@Param			id	path		string							true	"Workspace ID"
255//	@Success		200	{object}	proto.ProjectInitPromptResponse
256//	@Failure		404	{object}	proto.Error
257//	@Failure		500	{object}	proto.Error
258//	@Router			/workspaces/{id}/project/init-prompt [get]
259func (c *controllerV1) handleGetWorkspaceProjectInitPrompt(w http.ResponseWriter, r *http.Request) {
260	id := r.PathValue("id")
261	prompt, err := c.backend.InitializePrompt(id)
262	if err != nil {
263		c.handleError(w, r, err)
264		return
265	}
266	jsonEncode(w, proto.ProjectInitPromptResponse{Prompt: prompt})
267}
268
269// handleGetWorkspaceSkills returns the effective visible skills for a workspace.
270//
271//	@Summary		List visible skills
272//	@Tags			skills
273//	@Produce		json
274//	@Param			id	path		string				true	"Workspace ID"
275//	@Success		200	{array}		proto.SkillInfo
276//	@Failure		404	{object}	proto.Error
277//	@Failure		500	{object}	proto.Error
278//	@Router			/workspaces/{id}/skills [get]
279func (c *controllerV1) handleGetWorkspaceSkills(w http.ResponseWriter, r *http.Request) {
280	id := r.PathValue("id")
281	skills, err := c.backend.ListSkills(id)
282	if err != nil {
283		c.handleError(w, r, err)
284		return
285	}
286	jsonEncode(w, skills)
287}
288
289// handlePostWorkspaceSkillRead reads a skill's content by ID.
290//
291//	@Summary		Read skill content
292//	@Tags			skills
293//	@Accept			json
294//	@Produce		json
295//	@Param			id		path		string						true	"Workspace ID"
296//	@Param			request	body		proto.ReadSkillRequest		true	"Read skill request"
297//	@Success		200		{object}	proto.ReadSkillResponse
298//	@Failure		400		{object}	proto.Error
299//	@Failure		404		{object}	proto.Error
300//	@Failure		500		{object}	proto.Error
301//	@Router			/workspaces/{id}/skills/read [post]
302func (c *controllerV1) handlePostWorkspaceSkillRead(w http.ResponseWriter, r *http.Request) {
303	id := r.PathValue("id")
304
305	var req proto.ReadSkillRequest
306	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
307		c.server.logError(r, "Failed to decode request", "error", err)
308		jsonError(w, http.StatusBadRequest, "failed to decode request")
309		return
310	}
311
312	content, result, err := c.backend.ReadSkill(r.Context(), id, req.SkillID)
313	if err != nil {
314		c.handleError(w, r, err)
315		return
316	}
317	jsonEncode(w, proto.ReadSkillResponse{Content: content, Result: result})
318}
319
320// handlePostWorkspaceMCPEnableDocker enables the Docker MCP server.
321//
322//	@Summary		Enable Docker MCP
323//	@Tags			mcp
324//	@Param			id	path	string	true	"Workspace ID"
325//	@Success		200
326//	@Failure		404	{object}	proto.Error
327//	@Failure		500	{object}	proto.Error
328//	@Router			/workspaces/{id}/mcp/docker/enable [post]
329func (c *controllerV1) handlePostWorkspaceMCPEnableDocker(w http.ResponseWriter, r *http.Request) {
330	id := r.PathValue("id")
331	if err := c.backend.EnableDockerMCP(r.Context(), id); err != nil {
332		c.handleError(w, r, err)
333		return
334	}
335	w.WriteHeader(http.StatusOK)
336}
337
338// handlePostWorkspaceMCPDisableDocker disables the Docker MCP server.
339//
340//	@Summary		Disable Docker MCP
341//	@Tags			mcp
342//	@Param			id	path	string	true	"Workspace ID"
343//	@Success		200
344//	@Failure		404	{object}	proto.Error
345//	@Failure		500	{object}	proto.Error
346//	@Router			/workspaces/{id}/mcp/docker/disable [post]
347func (c *controllerV1) handlePostWorkspaceMCPDisableDocker(w http.ResponseWriter, r *http.Request) {
348	id := r.PathValue("id")
349	if err := c.backend.DisableDockerMCP(id); err != nil {
350		c.handleError(w, r, err)
351		return
352	}
353	w.WriteHeader(http.StatusOK)
354}
355
356// handlePostWorkspaceMCPRefreshTools refreshes tools for a named MCP server.
357//
358//	@Summary		Refresh MCP tools
359//	@Tags			mcp
360//	@Accept			json
361//	@Param			id		path	string					true	"Workspace ID"
362//	@Param			request	body	proto.MCPNameRequest	true	"MCP name request"
363//	@Success		200
364//	@Failure		400	{object}	proto.Error
365//	@Failure		404	{object}	proto.Error
366//	@Failure		500	{object}	proto.Error
367//	@Router			/workspaces/{id}/mcp/refresh-tools [post]
368func (c *controllerV1) handlePostWorkspaceMCPRefreshTools(w http.ResponseWriter, r *http.Request) {
369	id := r.PathValue("id")
370
371	var req proto.MCPNameRequest
372	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
373		c.server.logError(r, "Failed to decode request", "error", err)
374		jsonError(w, http.StatusBadRequest, "failed to decode request")
375		return
376	}
377
378	if err := c.backend.RefreshMCPTools(r.Context(), id, req.Name); err != nil {
379		c.handleError(w, r, err)
380		return
381	}
382	w.WriteHeader(http.StatusOK)
383}
384
385// handlePostWorkspaceMCPReadResource reads a resource from an MCP server.
386//
387//	@Summary		Read MCP resource
388//	@Tags			mcp
389//	@Accept			json
390//	@Produce		json
391//	@Param			id		path		string						true	"Workspace ID"
392//	@Param			request	body		proto.MCPReadResourceRequest	true	"MCP read resource request"
393//	@Success		200		{object}	object
394//	@Failure		400		{object}	proto.Error
395//	@Failure		404		{object}	proto.Error
396//	@Failure		500		{object}	proto.Error
397//	@Router			/workspaces/{id}/mcp/read-resource [post]
398func (c *controllerV1) handlePostWorkspaceMCPReadResource(w http.ResponseWriter, r *http.Request) {
399	id := r.PathValue("id")
400
401	var req proto.MCPReadResourceRequest
402	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
403		c.server.logError(r, "Failed to decode request", "error", err)
404		jsonError(w, http.StatusBadRequest, "failed to decode request")
405		return
406	}
407
408	contents, err := c.backend.ReadMCPResource(r.Context(), id, req.Name, req.URI)
409	if err != nil {
410		c.handleError(w, r, err)
411		return
412	}
413	jsonEncode(w, contents)
414}
415
416// handlePostWorkspaceMCPGetPrompt retrieves a prompt from an MCP server.
417//
418//	@Summary		Get MCP prompt
419//	@Tags			mcp
420//	@Accept			json
421//	@Produce		json
422//	@Param			id		path		string						true	"Workspace ID"
423//	@Param			request	body		proto.MCPGetPromptRequest	true	"MCP get prompt request"
424//	@Success		200		{object}	proto.MCPGetPromptResponse
425//	@Failure		400		{object}	proto.Error
426//	@Failure		404		{object}	proto.Error
427//	@Failure		500		{object}	proto.Error
428//	@Router			/workspaces/{id}/mcp/get-prompt [post]
429func (c *controllerV1) handlePostWorkspaceMCPGetPrompt(w http.ResponseWriter, r *http.Request) {
430	id := r.PathValue("id")
431
432	var req proto.MCPGetPromptRequest
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	prompt, err := c.backend.GetMCPPrompt(id, req.ClientID, req.PromptID, req.Args)
440	if err != nil {
441		c.handleError(w, r, err)
442		return
443	}
444	jsonEncode(w, proto.MCPGetPromptResponse{Prompt: prompt})
445}
446
447// handleGetWorkspaceMCPStates returns the state of all MCP clients.
448//
449//	@Summary		Get MCP client states
450//	@Tags			mcp
451//	@Produce		json
452//	@Param			id	path		string						true	"Workspace ID"
453//	@Success		200	{object}	map[string]proto.MCPClientInfo
454//	@Failure		404	{object}	proto.Error
455//	@Failure		500	{object}	proto.Error
456//	@Router			/workspaces/{id}/mcp/states [get]
457func (c *controllerV1) handleGetWorkspaceMCPStates(w http.ResponseWriter, r *http.Request) {
458	id := r.PathValue("id")
459	states := c.backend.MCPGetStates(id)
460	result := make(map[string]proto.MCPClientInfo, len(states))
461	for k, v := range states {
462		result[k] = proto.MCPClientInfo{
463			Name:          v.Name,
464			State:         proto.MCPState(v.State),
465			Error:         v.Error,
466			ToolCount:     v.Counts.Tools,
467			PromptCount:   v.Counts.Prompts,
468			ResourceCount: v.Counts.Resources,
469			ConnectedAt:   v.ConnectedAt,
470		}
471	}
472	jsonEncode(w, result)
473}
474
475// handlePostWorkspaceMCPRefreshPrompts refreshes prompts for a named MCP server.
476//
477//	@Summary		Refresh MCP prompts
478//	@Tags			mcp
479//	@Accept			json
480//	@Param			id		path	string					true	"Workspace ID"
481//	@Param			request	body	proto.MCPNameRequest	true	"MCP name request"
482//	@Success		200
483//	@Failure		400	{object}	proto.Error
484//	@Failure		404	{object}	proto.Error
485//	@Failure		500	{object}	proto.Error
486//	@Router			/workspaces/{id}/mcp/refresh-prompts [post]
487func (c *controllerV1) handlePostWorkspaceMCPRefreshPrompts(w http.ResponseWriter, r *http.Request) {
488	id := r.PathValue("id")
489
490	var req proto.MCPNameRequest
491	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
492		c.server.logError(r, "Failed to decode request", "error", err)
493		jsonError(w, http.StatusBadRequest, "failed to decode request")
494		return
495	}
496
497	c.backend.MCPRefreshPrompts(r.Context(), id, req.Name)
498	w.WriteHeader(http.StatusOK)
499}
500
501// handlePostWorkspaceMCPRefreshResources refreshes resources for a named MCP server.
502//
503//	@Summary		Refresh MCP resources
504//	@Tags			mcp
505//	@Accept			json
506//	@Param			id		path	string					true	"Workspace ID"
507//	@Param			request	body	proto.MCPNameRequest	true	"MCP name request"
508//	@Success		200
509//	@Failure		400	{object}	proto.Error
510//	@Failure		404	{object}	proto.Error
511//	@Failure		500	{object}	proto.Error
512//	@Router			/workspaces/{id}/mcp/refresh-resources [post]
513func (c *controllerV1) handlePostWorkspaceMCPRefreshResources(w http.ResponseWriter, r *http.Request) {
514	id := r.PathValue("id")
515
516	var req proto.MCPNameRequest
517	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
518		c.server.logError(r, "Failed to decode request", "error", err)
519		jsonError(w, http.StatusBadRequest, "failed to decode request")
520		return
521	}
522
523	c.backend.MCPRefreshResources(r.Context(), id, req.Name)
524	w.WriteHeader(http.StatusOK)
525}