config.go 15 KB

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