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,111 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"task/internal/daemon/api"
|
||||
)
|
||||
|
||||
// Client talks to the local JSON daemon over a Unix socket.
|
||||
type Client struct {
|
||||
http *http.Client
|
||||
kind string // tui | cli | mcp
|
||||
token string // bearer for mcp
|
||||
project string // mcp bound project
|
||||
}
|
||||
|
||||
// Options configures a daemon client.
|
||||
type Options struct {
|
||||
Socket string
|
||||
Kind string
|
||||
Token string
|
||||
Project string
|
||||
}
|
||||
|
||||
// Dial creates a client for the given Unix socket.
|
||||
func Dial(opts Options) *Client {
|
||||
kind := opts.Kind
|
||||
if kind == "" {
|
||||
kind = api.ClientTUI
|
||||
}
|
||||
return &Client{
|
||||
kind: kind,
|
||||
token: opts.Token,
|
||||
project: opts.Project,
|
||||
http: &http.Client{
|
||||
Timeout: 60 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "unix", opts.Socket)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) do(method, path string, in any, out any) error {
|
||||
var body io.Reader
|
||||
if in != nil {
|
||||
b, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body = bytes.NewReader(b)
|
||||
}
|
||||
req, err := http.NewRequest(method, "http://daemon"+path, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if in != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
req.Header.Set(api.HeaderClient, c.kind)
|
||||
if c.kind == api.ClientMCP {
|
||||
if c.token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
}
|
||||
if c.project != "" {
|
||||
req.Header.Set("X-TM-Project", c.project)
|
||||
}
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
var er struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
_ = json.Unmarshal(data, &er)
|
||||
if er.Error == "" {
|
||||
er.Error = strings.TrimSpace(string(data))
|
||||
}
|
||||
if er.Error == "" {
|
||||
er.Error = resp.Status
|
||||
}
|
||||
return fmt.Errorf("%s", er.Error)
|
||||
}
|
||||
if out == nil || resp.StatusCode == http.StatusNoContent || len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(data, out)
|
||||
}
|
||||
|
||||
// Health checks /v1/health.
|
||||
func (c *Client) Health() error {
|
||||
return c.do(http.MethodGet, "/v1/health", nil, nil)
|
||||
}
|
||||
Reference in New Issue
Block a user