Lets agents search/navigate memory via the CLI, as the first step toward deprecating the memory MCP. claude-memory is a FastAPI service (the MCP is just one frontend); homelab memory is a thin Bearer-auth HTTP client over the same API, using the env the hooks already set (CLAUDE_MEMORY_API_URL/KEY). It works even when the MCP frontend is down — the recurring disconnect that took the MCP offline for this whole session. Verbs: recall (server-side semantic search), list, categories, tags, stats, secret (read); store, update, delete (write). Validated against the live API including a store→recall→delete round-trip — full data-plane parity with the MCP. The deprecation itself (rewiring the per-prompt auto-recall + auto-learn hooks to the CLI, then uninstalling the MCP) is a deliberate follow-up, sequenced after the CLI is proven in the hooks — see docs/adr/0008. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
103 lines
2.8 KiB
Go
103 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"`
|
|
}
|