104 lines
2.8 KiB
Go
104 lines
2.8 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"os"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// defaultMemoryURL is used when no env override is present (agents normally have
|
||
|
|
// CLAUDE_MEMORY_API_URL set by the memory hooks).
|
||
|
|
const defaultMemoryURL = "https://claude-memory.viktorbarzin.me"
|
||
|
|
|
||
|
|
type memoryClient struct {
|
||
|
|
base string
|
||
|
|
key string
|
||
|
|
http *http.Client
|
||
|
|
}
|
||
|
|
|
||
|
|
func firstEnv(keys ...string) string {
|
||
|
|
for _, k := range keys {
|
||
|
|
if v := os.Getenv(k); v != "" {
|
||
|
|
return v
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
func resolveMemoryBase() string {
|
||
|
|
if b := firstEnv("CLAUDE_MEMORY_API_URL", "MEMORY_API_URL"); b != "" {
|
||
|
|
return strings.TrimRight(b, "/")
|
||
|
|
}
|
||
|
|
return defaultMemoryURL
|
||
|
|
}
|
||
|
|
|
||
|
|
// newMemoryClient talks straight to the claude-memory HTTP API (the same backend
|
||
|
|
// the MCP wraps), so it works even when the MCP frontend is down.
|
||
|
|
func newMemoryClient() (*memoryClient, error) {
|
||
|
|
key := firstEnv("CLAUDE_MEMORY_API_KEY", "MEMORY_API_KEY")
|
||
|
|
if key == "" {
|
||
|
|
return nil, fmt.Errorf("no memory API key — set CLAUDE_MEMORY_API_KEY (or MEMORY_API_KEY)")
|
||
|
|
}
|
||
|
|
return &memoryClient{base: resolveMemoryBase(), key: key, http: &http.Client{Timeout: 30 * time.Second}}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *memoryClient) do(method, path string, body interface{}) ([]byte, error) {
|
||
|
|
var r io.Reader
|
||
|
|
if body != nil {
|
||
|
|
b, err := json.Marshal(body)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
r = bytes.NewReader(b)
|
||
|
|
}
|
||
|
|
req, err := http.NewRequest(method, c.base+path, r)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
req.Header.Set("Authorization", "Bearer "+c.key)
|
||
|
|
if body != nil {
|
||
|
|
req.Header.Set("Content-Type", "application/json")
|
||
|
|
}
|
||
|
|
resp, err := c.http.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
out, _ := io.ReadAll(resp.Body)
|
||
|
|
if resp.StatusCode >= 300 {
|
||
|
|
return nil, fmt.Errorf("memory API %s %s -> %d: %s", method, path, resp.StatusCode, strings.TrimSpace(string(out)))
|
||
|
|
}
|
||
|
|
return out, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Request bodies mirror src/claude_memory/api/models.py.
|
||
|
|
|
||
|
|
type memRecallReq struct {
|
||
|
|
Context string `json:"context"`
|
||
|
|
ExpandedQuery string `json:"expanded_query,omitempty"`
|
||
|
|
Category string `json:"category,omitempty"`
|
||
|
|
SortBy string `json:"sort_by,omitempty"`
|
||
|
|
Limit int `json:"limit,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type memStoreReq struct {
|
||
|
|
Content string `json:"content"`
|
||
|
|
Category string `json:"category,omitempty"`
|
||
|
|
Tags string `json:"tags,omitempty"`
|
||
|
|
ExpandedKeywords string `json:"expanded_keywords,omitempty"`
|
||
|
|
Importance float64 `json:"importance"`
|
||
|
|
ForceSensitive bool `json:"force_sensitive,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type memUpdateReq struct {
|
||
|
|
Content *string `json:"content,omitempty"`
|
||
|
|
Tags *string `json:"tags,omitempty"`
|
||
|
|
Importance *float64 `json:"importance,omitempty"`
|
||
|
|
ExpandedKeywords *string `json:"expanded_keywords,omitempty"`
|
||
|
|
}
|