@@ -0,0 +1,656 @@
13+ "github.com/charmbracelet/crush/internal/app"
14+ "github.com/charmbracelet/crush/internal/config"
15+ "github.com/charmbracelet/crush/internal/db"
16+ "github.com/charmbracelet/crush/internal/permission"
17+ "github.com/charmbracelet/crush/internal/proto"
18+ "github.com/charmbracelet/crush/internal/session"
19+ "github.com/charmbracelet/crush/internal/version"
20+ "github.com/google/uuid"
23+type controllerV1 struct {
27+func (c *controllerV1) handleGetHealth(w http.ResponseWriter, _ *http.Request) {
28+ w.WriteHeader(http.StatusOK)
31+func (c *controllerV1) handleGetVersion(w http.ResponseWriter, _ *http.Request) {
32+ jsonEncode(w, proto.VersionInfo{
33+ Version: version.Version,
34+ Commit: version.Commit,
35+ GoVersion: runtime.Version(),
36+ Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
40+func (c *controllerV1) handlePostControl(w http.ResponseWriter, r *http.Request) {
41+ var req proto.ServerControl
42+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
43+ c.logError(r, "Failed to decode request", "error", err)
44+ jsonError(w, http.StatusBadRequest, "failed to decode request")
51+ slog.Info("Shutting down server...")
52+ if err := c.Shutdown(context.Background()); err != nil {
53+ slog.Error("Failed to shutdown server", "error", err)
57+ c.logError(r, "Unknown command", "command", req.Command)
58+ jsonError(w, http.StatusBadRequest, "unknown command")
63+func (c *controllerV1) handleGetConfig(w http.ResponseWriter, _ *http.Request) {
67+func (c *controllerV1) handleGetWorkspaces(w http.ResponseWriter, _ *http.Request) {
68+ workspaces := []proto.Workspace{}
69+ for _, ws := range c.workspaces.Seq2() {
70+ workspaces = append(workspaces, proto.Workspace{
73+ YOLO: ws.cfg.Permissions != nil && ws.cfg.Permissions.SkipRequests,
74+ DataDir: ws.cfg.Options.DataDirectory,
75+ Debug: ws.cfg.Options.Debug,
79+ jsonEncode(w, workspaces)
82+func (c *controllerV1) handleGetWorkspaceLSPDiagnostics(w http.ResponseWriter, r *http.Request) {
83+ id := r.PathValue("id")
84+ ws, ok := c.workspaces.Get(id)
86+ c.logError(r, "Workspace not found", "id", id)
87+ jsonError(w, http.StatusNotFound, "workspace not found")
91+ lspName := r.PathValue("lsp")
93+ for name, client := range ws.LSPManager.Clients().Seq2() {
95+ diagnostics := client.GetDiagnostics()
96+ jsonEncode(w, diagnostics)
103+ c.logError(r, "LSP client not found", "id", id, "lsp", lspName)
104+ jsonError(w, http.StatusNotFound, "LSP client not found")
108+func (c *controllerV1) handleGetWorkspaceLSPs(w http.ResponseWriter, r *http.Request) {
109+ id := r.PathValue("id")
110+ _, ok := c.workspaces.Get(id)
112+ c.logError(r, "Workspace not found", "id", id)
113+ jsonError(w, http.StatusNotFound, "workspace not found")
117+ lspClients := app.GetLSPStates()
118+ jsonEncode(w, lspClients)
121+func (c *controllerV1) handleGetWorkspaceAgentSessionPromptQueued(w http.ResponseWriter, r *http.Request) {
122+ id := r.PathValue("id")
123+ ws, ok := c.workspaces.Get(id)
125+ c.logError(r, "Workspace not found", "id", id)
126+ jsonError(w, http.StatusNotFound, "workspace not found")
130+ sid := r.PathValue("sid")
131+ queued := ws.App.AgentCoordinator.QueuedPrompts(sid)
132+ jsonEncode(w, queued)
135+func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.ResponseWriter, r *http.Request) {
136+ id := r.PathValue("id")
137+ ws, ok := c.workspaces.Get(id)
139+ c.logError(r, "Workspace not found", "id", id)
140+ jsonError(w, http.StatusNotFound, "workspace not found")
144+ sid := r.PathValue("sid")
145+ ws.App.AgentCoordinator.ClearQueue(sid)
146+ w.WriteHeader(http.StatusOK)
149+func (c *controllerV1) handleGetWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
150+ id := r.PathValue("id")
151+ ws, ok := c.workspaces.Get(id)
153+ c.logError(r, "Workspace not found", "id", id)
154+ jsonError(w, http.StatusNotFound, "workspace not found")
158+ sid := r.PathValue("sid")
159+ if err := ws.App.AgentCoordinator.Summarize(r.Context(), sid); err != nil {
160+ c.logError(r, "Failed to summarize session", "error", err, "id", id, "sid", sid)
161+ jsonError(w, http.StatusInternalServerError, "failed to summarize session")
166+func (c *controllerV1) handlePostWorkspaceAgentSessionCancel(w http.ResponseWriter, r *http.Request) {
167+ id := r.PathValue("id")
168+ ws, ok := c.workspaces.Get(id)
170+ c.logError(r, "Workspace not found", "id", id)
171+ jsonError(w, http.StatusNotFound, "workspace not found")
175+ sid := r.PathValue("sid")
176+ if ws.App.AgentCoordinator != nil {
177+ ws.App.AgentCoordinator.Cancel(sid)
179+ w.WriteHeader(http.StatusOK)
182+func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) {
183+ id := r.PathValue("id")
184+ ws, ok := c.workspaces.Get(id)
186+ c.logError(r, "Workspace not found", "id", id)
187+ jsonError(w, http.StatusNotFound, "workspace not found")
191+ sid := r.PathValue("sid")
192+ se, err := ws.App.Sessions.Get(r.Context(), sid)
194+ c.logError(r, "Failed to get session", "error", err, "id", id, "sid", sid)
195+ jsonError(w, http.StatusInternalServerError, "failed to get session")
199+ var isSessionBusy bool
200+ if ws.App.AgentCoordinator != nil {
201+ isSessionBusy = ws.App.AgentCoordinator.IsSessionBusy(sid)
204+ jsonEncode(w, proto.AgentSession{
205+ Session: proto.Session{
209+ IsBusy: isSessionBusy,
213+func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
214+ id := r.PathValue("id")
215+ ws, ok := c.workspaces.Get(id)
217+ c.logError(r, "Workspace not found", "id", id)
218+ jsonError(w, http.StatusNotFound, "workspace not found")
222+ w.Header().Set("Accept", "application/json")
224+ var msg proto.AgentMessage
225+ if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
226+ c.logError(r, "Failed to decode request", "error", err)
227+ jsonError(w, http.StatusBadRequest, "failed to decode request")
231+ if ws.App.AgentCoordinator == nil {
232+ c.logError(r, "Agent coordinator not initialized", "id", id)
233+ jsonError(w, http.StatusBadRequest, "agent coordinator not initialized")
237+ if _, err := ws.App.AgentCoordinator.Run(c.ctx, msg.SessionID, msg.Prompt); err != nil {
238+ c.logError(r, "Failed to enqueue message", "error", err, "id", id, "sid", msg.SessionID)
239+ jsonError(w, http.StatusInternalServerError, "failed to enqueue message")
244+func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
245+ id := r.PathValue("id")
246+ ws, ok := c.workspaces.Get(id)
248+ c.logError(r, "Workspace not found", "id", id)
249+ jsonError(w, http.StatusNotFound, "workspace not found")
253+ var agentInfo proto.AgentInfo
254+ if ws.App.AgentCoordinator != nil {
255+ m := ws.App.AgentCoordinator.Model()
256+ agentInfo = proto.AgentInfo{
257+ Model: m.CatwalkCfg,
258+ IsBusy: ws.App.AgentCoordinator.IsBusy(),
261+ jsonEncode(w, agentInfo)
264+func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) {
265+ id := r.PathValue("id")
266+ ws, ok := c.workspaces.Get(id)
268+ c.logError(r, "Workspace not found", "id", id)
269+ jsonError(w, http.StatusNotFound, "workspace not found")
273+ if err := ws.App.UpdateAgentModel(r.Context()); err != nil {
274+ c.logError(r, "Failed to update agent model", "error", err)
275+ jsonError(w, http.StatusInternalServerError, "failed to update agent model")
280+func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) {
281+ id := r.PathValue("id")
282+ ws, ok := c.workspaces.Get(id)
284+ c.logError(r, "Workspace not found", "id", id)
285+ jsonError(w, http.StatusNotFound, "workspace not found")
289+ if err := ws.App.InitCoderAgent(r.Context()); err != nil {
290+ c.logError(r, "Failed to initialize coder agent", "error", err)
291+ jsonError(w, http.StatusInternalServerError, "failed to initialize coder agent")
296+func (c *controllerV1) handleGetWorkspaceSessionHistory(w http.ResponseWriter, r *http.Request) {
297+ id := r.PathValue("id")
298+ ws, ok := c.workspaces.Get(id)
300+ c.logError(r, "Workspace not found", "id", id)
301+ jsonError(w, http.StatusNotFound, "workspace not found")
305+ sid := r.PathValue("sid")
306+ historyItems, err := ws.App.History.ListBySession(r.Context(), sid)
308+ c.logError(r, "Failed to list history", "error", err, "id", id, "sid", sid)
309+ jsonError(w, http.StatusInternalServerError, "failed to list history")
313+ jsonEncode(w, historyItems)
316+func (c *controllerV1) handleGetWorkspaceSessionMessages(w http.ResponseWriter, r *http.Request) {
317+ id := r.PathValue("id")
318+ ws, ok := c.workspaces.Get(id)
320+ c.logError(r, "Workspace not found", "id", id)
321+ jsonError(w, http.StatusNotFound, "workspace not found")
325+ sid := r.PathValue("sid")
326+ messages, err := ws.App.Messages.List(r.Context(), sid)
328+ c.logError(r, "Failed to list messages", "error", err, "id", id, "sid", sid)
329+ jsonError(w, http.StatusInternalServerError, "failed to list messages")
333+ jsonEncode(w, messages)
336+func (c *controllerV1) handleGetWorkspaceSession(w http.ResponseWriter, r *http.Request) {
337+ id := r.PathValue("id")
338+ ws, ok := c.workspaces.Get(id)
340+ c.logError(r, "Workspace not found", "id", id)
341+ jsonError(w, http.StatusNotFound, "workspace not found")
345+ sid := r.PathValue("sid")
346+ sess, err := ws.App.Sessions.Get(r.Context(), sid)
348+ c.logError(r, "Failed to get session", "error", err, "id", id, "sid", sid)
349+ jsonError(w, http.StatusInternalServerError, "failed to get session")
356+func (c *controllerV1) handlePostWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
357+ id := r.PathValue("id")
358+ ws, ok := c.workspaces.Get(id)
360+ c.logError(r, "Workspace not found", "id", id)
361+ jsonError(w, http.StatusNotFound, "workspace not found")
365+ var args session.Session
366+ if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
367+ c.logError(r, "Failed to decode request", "error", err)
368+ jsonError(w, http.StatusBadRequest, "failed to decode request")
372+ sess, err := ws.App.Sessions.Create(r.Context(), args.Title)
374+ c.logError(r, "Failed to create session", "error", err, "id", id)
375+ jsonError(w, http.StatusInternalServerError, "failed to create session")
382+func (c *controllerV1) handleGetWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
383+ id := r.PathValue("id")
384+ ws, ok := c.workspaces.Get(id)
386+ c.logError(r, "Workspace not found", "id", id)
387+ jsonError(w, http.StatusNotFound, "workspace not found")
391+ sessions, err := ws.App.Sessions.List(r.Context())
393+ c.logError(r, "Failed to list sessions", "error", err)
394+ jsonError(w, http.StatusInternalServerError, "failed to list sessions")
398+ jsonEncode(w, sessions)
401+func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter, r *http.Request) {
402+ id := r.PathValue("id")
403+ ws, ok := c.workspaces.Get(id)
405+ c.logError(r, "Workspace not found", "id", id)
406+ jsonError(w, http.StatusNotFound, "workspace not found")
410+ var req proto.PermissionGrant
411+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
412+ c.logError(r, "Failed to decode request", "error", err)
413+ jsonError(w, http.StatusBadRequest, "failed to decode request")
417+ perm := permission.PermissionRequest{
418+ ID: req.Permission.ID,
419+ SessionID: req.Permission.SessionID,
420+ ToolCallID: req.Permission.ToolCallID,
421+ ToolName: req.Permission.ToolName,
422+ Description: req.Permission.Description,
423+ Action: req.Permission.Action,
424+ Params: req.Permission.Params,
425+ Path: req.Permission.Path,
429+ case proto.PermissionAllow:
430+ ws.App.Permissions.Grant(perm)
431+ case proto.PermissionAllowForSession:
432+ ws.App.Permissions.GrantPersistent(perm)
433+ case proto.PermissionDeny:
434+ ws.App.Permissions.Deny(perm)
436+ c.logError(r, "Invalid permission action", "action", req.Action)
437+ jsonError(w, http.StatusBadRequest, "invalid permission action")
442+func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
443+ id := r.PathValue("id")
444+ ws, ok := c.workspaces.Get(id)
446+ c.logError(r, "Workspace not found", "id", id)
447+ jsonError(w, http.StatusNotFound, "workspace not found")
451+ var req proto.PermissionSkipRequest
452+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
453+ c.logError(r, "Failed to decode request", "error", err)
454+ jsonError(w, http.StatusBadRequest, "failed to decode request")
458+ ws.App.Permissions.SetSkipRequests(req.Skip)
461+func (c *controllerV1) handleGetWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
462+ id := r.PathValue("id")
463+ ws, ok := c.workspaces.Get(id)
465+ c.logError(r, "Workspace not found", "id", id)
466+ jsonError(w, http.StatusNotFound, "workspace not found")
470+ skip := ws.App.Permissions.SkipRequests()
471+ jsonEncode(w, proto.PermissionSkipRequest{Skip: skip})
474+func (c *controllerV1) handleGetWorkspaceProviders(w http.ResponseWriter, r *http.Request) {
475+ id := r.PathValue("id")
476+ ws, ok := c.workspaces.Get(id)
478+ c.logError(r, "Workspace not found", "id", id)
479+ jsonError(w, http.StatusNotFound, "workspace not found")
483+ providers, _ := config.Providers(ws.cfg)
484+ jsonEncode(w, providers)
487+func (c *controllerV1) handleGetWorkspaceEvents(w http.ResponseWriter, r *http.Request) {
488+ flusher := http.NewResponseController(w)
489+ id := r.PathValue("id")
490+ ws, ok := c.workspaces.Get(id)
492+ c.logError(r, "Workspace not found", "id", id)
493+ jsonError(w, http.StatusNotFound, "workspace not found")
497+ w.Header().Set("Content-Type", "text/event-stream")
498+ w.Header().Set("Cache-Control", "no-cache")
499+ w.Header().Set("Connection", "keep-alive")
501+ events := ws.App.Events()
505+ case <-r.Context().Done():
506+ c.logDebug(r, "Stopping event stream")
508+ case ev, ok := <-events:
512+ c.logDebug(r, "Sending event", "event", fmt.Sprintf("%T %+v", ev, ev))
513+ data, err := json.Marshal(ev)
515+ c.logError(r, "Failed to marshal event", "error", err)
519+ fmt.Fprintf(w, "data: %s\n\n", data)
525+func (c *controllerV1) handleGetWorkspaceConfig(w http.ResponseWriter, r *http.Request) {
526+ id := r.PathValue("id")
527+ ws, ok := c.workspaces.Get(id)
529+ c.logError(r, "Workspace not found", "id", id)
530+ jsonError(w, http.StatusNotFound, "workspace not found")
534+ jsonEncode(w, ws.cfg)
537+func (c *controllerV1) handleDeleteWorkspaces(w http.ResponseWriter, r *http.Request) {
538+ id := r.PathValue("id")
539+ ws, ok := c.workspaces.Get(id)
543+ c.workspaces.Del(id)
546+func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
547+ id := r.PathValue("id")
548+ ws, ok := c.workspaces.Get(id)
550+ c.logError(r, "Workspace not found", "id", id)
551+ jsonError(w, http.StatusNotFound, "workspace not found")
555+ jsonEncode(w, proto.Workspace{
558+ YOLO: ws.cfg.Permissions != nil && ws.cfg.Permissions.SkipRequests,
559+ DataDir: ws.cfg.Options.DataDirectory,
560+ Debug: ws.cfg.Options.Debug,
565+func (c *controllerV1) handlePostWorkspaces(w http.ResponseWriter, r *http.Request) {
566+ var args proto.Workspace
567+ if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
568+ c.logError(r, "Failed to decode request", "error", err)
569+ jsonError(w, http.StatusBadRequest, "failed to decode request")
573+ if args.Path == "" {
574+ c.logError(r, "Path is required")
575+ jsonError(w, http.StatusBadRequest, "path is required")
579+ id := uuid.New().String()
580+ cfg, err := config.Init(args.Path, args.DataDir, args.Debug)
582+ c.logError(r, "Failed to initialize config", "error", err)
583+ jsonError(w, http.StatusBadRequest, fmt.Sprintf("failed to initialize config: %v", err))
587+ if cfg.Permissions == nil {
588+ cfg.Permissions = &config.Permissions{}
590+ cfg.Permissions.SkipRequests = args.YOLO
592+ if err := createDotCrushDir(cfg.Options.DataDirectory); err != nil {
593+ c.logError(r, "Failed to create data directory", "error", err)
594+ jsonError(w, http.StatusInternalServerError, "failed to create data directory")
598+ conn, err := db.Connect(c.ctx, cfg.Options.DataDirectory)
600+ c.logError(r, "Failed to connect to database", "error", err)
601+ jsonError(w, http.StatusInternalServerError, "failed to connect to database")
605+ appWorkspace, err := app.New(c.ctx, conn, cfg)
607+ slog.Error("Failed to create app workspace", "error", err)
608+ jsonError(w, http.StatusInternalServerError, "failed to create app workspace")
620+ c.workspaces.Set(id, ws)
621+ jsonEncode(w, proto.Workspace{
624+ DataDir: cfg.Options.DataDirectory,
625+ Debug: cfg.Options.Debug,
626+ YOLO: cfg.Permissions.SkipRequests,
632+func createDotCrushDir(dir string) error {
633+ if err := os.MkdirAll(dir, 0o700); err != nil {
634+ return fmt.Errorf("failed to create data directory: %q %w", dir, err)
637+ gitIgnorePath := filepath.Join(dir, ".gitignore")
638+ if _, err := os.Stat(gitIgnorePath); os.IsNotExist(err) {
639+ if err := os.WriteFile(gitIgnorePath, []byte("*\n"), 0o644); err != nil {
640+ return fmt.Errorf("failed to create .gitignore file: %q %w", gitIgnorePath, err)
647+func jsonEncode(w http.ResponseWriter, v any) {
648+ w.Header().Set("Content-Type", "application/json")
649+ _ = json.NewEncoder(w).Encode(v)
652+func jsonError(w http.ResponseWriter, status int, message string) {
653+ w.Header().Set("Content-Type", "application/json")
654+ w.WriteHeader(status)
655+ _ = json.NewEncoder(w).Encode(proto.Error{Message: message})