homelab: add memory verb-group (v0.3.0) — direct claude-memory HTTP client
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>
This commit is contained in:
parent
3594485f77
commit
48b63ffa6f
8 changed files with 514 additions and 3 deletions
103
cli/memory.go
Normal file
103
cli/memory.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
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"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue