Add local JSON daemon and route TUI/CLI through it.
A per-DataDir background process owns ticket JSON, history, and sync so multiple TUI instances stay consistent; MCP token APIs are ready for phase 2. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"task/internal/project"
|
||||
)
|
||||
|
||||
func (s *Server) handleListProjects(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := s.projects.List()
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if ClientKind(r.Context()) == ClientMCP {
|
||||
pid := MCPProjectID(r.Context())
|
||||
filtered := make([]project.Project, 0, 1)
|
||||
for _, p := range list {
|
||||
if p.ID == pid {
|
||||
filtered = append(filtered, p)
|
||||
}
|
||||
}
|
||||
list = filtered
|
||||
}
|
||||
writeJSON(w, http.StatusOK, list)
|
||||
}
|
||||
|
||||
func (s *Server) handleEnsureProject(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := readJSON(r, &body); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if !requireMCPProject(r, body.ID) {
|
||||
writeErr(w, http.StatusForbidden, "mcp token project mismatch")
|
||||
return
|
||||
}
|
||||
if err := s.projects.Ensure(body.ID); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
s.notify(body.ID)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"id": body.ID})
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteProject(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if !requireMCPProject(r, id) {
|
||||
writeErr(w, http.StatusForbidden, "mcp token project mismatch")
|
||||
return
|
||||
}
|
||||
if err := s.timeStore.DeleteByProject(id); err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if t, err := s.timeStore.LoadActiveTimer(); err == nil && t != nil && t.ProjectID == id {
|
||||
_ = s.timeStore.SaveActiveTimer(nil)
|
||||
}
|
||||
if err := s.projects.Delete(id); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
s.notify(id)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) handleGetMeta(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if !requireMCPProject(r, id) {
|
||||
writeErr(w, http.StatusForbidden, "mcp token project mismatch")
|
||||
return
|
||||
}
|
||||
meta, err := s.projects.LoadMeta(id)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, meta)
|
||||
}
|
||||
|
||||
func (s *Server) handleSaveMeta(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if !requireMCPProject(r, id) {
|
||||
writeErr(w, http.StatusForbidden, "mcp token project mismatch")
|
||||
return
|
||||
}
|
||||
var meta project.Meta
|
||||
if err := readJSON(r, &meta); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
// Preserve existing mcpAuth if client omitted it (TUI sync edit).
|
||||
prev, _ := s.projects.LoadMeta(id)
|
||||
if meta.McpAuth == nil && prev.McpAuth != nil {
|
||||
meta.McpAuth = prev.McpAuth
|
||||
}
|
||||
if err := s.projects.SaveMeta(id, meta); err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
s.notify(id)
|
||||
writeJSON(w, http.StatusOK, meta)
|
||||
}
|
||||
|
||||
func (s *Server) handleMCPTokenStatus(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if ClientKind(r.Context()) == ClientMCP {
|
||||
writeErr(w, http.StatusForbidden, "mcp cannot inspect token status")
|
||||
return
|
||||
}
|
||||
meta, err := s.projects.LoadMeta(id)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
configured := meta.McpAuth != nil && meta.McpAuth.TokenHash != ""
|
||||
created := ""
|
||||
if meta.McpAuth != nil {
|
||||
created = meta.McpAuth.CreatedAt
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"configured": configured,
|
||||
"createdAt": created,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleMCPTokenCreate(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if ClientKind(r.Context()) == ClientMCP {
|
||||
writeErr(w, http.StatusForbidden, "mcp cannot create tokens")
|
||||
return
|
||||
}
|
||||
if err := s.projects.Ensure(id); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
raw := make([]byte, 24)
|
||||
if _, err := rand.Read(raw); err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
token := "tm_mcp_" + hex.EncodeToString(raw)
|
||||
meta, err := s.projects.LoadMeta(id)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
created := time.Now().UTC().Format(time.RFC3339)
|
||||
meta.McpAuth = &project.McpAuth{TokenHash: hashToken(token), CreatedAt: created}
|
||||
if err := s.projects.SaveMeta(id, meta); err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"token": token,
|
||||
"project": id,
|
||||
"createdAt": created,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleMCPTokenRevoke(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if ClientKind(r.Context()) == ClientMCP {
|
||||
writeErr(w, http.StatusForbidden, "mcp cannot revoke tokens")
|
||||
return
|
||||
}
|
||||
meta, err := s.projects.LoadMeta(id)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
meta.McpAuth = nil
|
||||
if err := s.projects.SaveMeta(id, meta); err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
Reference in New Issue
Block a user