package main import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "time" ) const defaultBrainURL = "http://koala:30330" // brainClient calls the brain HTTP REST API exposed alongside the MCP // endpoint at the same host:port. /mcp serves MCP framing; /query and /write // serve plain REST. We use the REST surface because the CLI is a // shell-friendly client; MCP framing is unnecessary. type brainClient struct { baseURL string http *http.Client } func newBrainClient() *brainClient { u := os.Getenv("BRAIN_URL") if u == "" { u = defaultBrainURL } return &brainClient{ baseURL: u, http: &http.Client{Timeout: 5 * time.Second}, } } // QueryHit mirrors a single result from the brain's /query endpoint. type QueryHit struct { Path string `json:"path"` Title string `json:"title"` Excerpt string `json:"excerpt"` Score int `json:"score"` } // QueryResult mirrors the /query response envelope. type QueryResult struct { Results []QueryHit `json:"results"` } func (c *brainClient) Query(ctx context.Context, topic string, limit int) (*QueryResult, error) { payload, err := json.Marshal(struct { Query string `json:"query"` Limit int `json:"limit"` }{Query: topic, Limit: limit}) if err != nil { return nil, fmt.Errorf("marshal payload: %w", err) } u := c.baseURL + "/query" req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewReader(payload)) if err != nil { return nil, fmt.Errorf("build request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := c.http.Do(req) if err != nil { return nil, fmt.Errorf("brain POST /query: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("brain POST /query: status %d: %s", resp.StatusCode, string(body)) } var out QueryResult if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return nil, fmt.Errorf("decode /query response: %w", err) } return &out, nil } // WriteResult mirrors the /write response envelope. type WriteResult struct { Path string `json:"path"` } func (c *brainClient) Write(ctx context.Context, kind, slug string, content io.Reader) (*WriteResult, error) { body, err := io.ReadAll(content) if err != nil { return nil, fmt.Errorf("read content: %w", err) } payload, err := json.Marshal(struct { Type string `json:"type"` Slug string `json:"slug"` Content string `json:"content"` }{Type: kind, Slug: slug, Content: string(body)}) if err != nil { return nil, fmt.Errorf("marshal payload: %w", err) } u := c.baseURL + "/write" req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewReader(payload)) if err != nil { return nil, fmt.Errorf("build request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := c.http.Do(req) if err != nil { return nil, fmt.Errorf("brain POST /write: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { respBody, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("brain POST /write: status %d: %s", resp.StatusCode, string(respBody)) } var out WriteResult if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return nil, fmt.Errorf("decode /write response: %w", err) } return &out, nil } // PassRateResult mirrors the /pass-rate response envelope. type PassRateResult struct { Skill string `json:"skill"` Window string `json:"window"` Pass int `json:"pass"` Fail int `json:"fail"` Skip int `json:"skip"` Total int `json:"total"` PassRate *float64 `json:"pass_rate"` } func (c *brainClient) PassRate(ctx context.Context, skill, window string) (*PassRateResult, error) { q := url.Values{} q.Set("skill", skill) q.Set("window", window) u := c.baseURL + "/pass-rate?" + q.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return nil, fmt.Errorf("build request: %w", err) } resp, err := c.http.Do(req) if err != nil { return nil, fmt.Errorf("brain GET /pass-rate: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("brain GET /pass-rate: status %d: %s", resp.StatusCode, string(body)) } var out PassRateResult if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return nil, fmt.Errorf("decode /pass-rate response: %w", err) } return &out, nil }