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) }