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>
112 lines
2.2 KiB
Go
112 lines
2.2 KiB
Go
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)
|
|
}
|