infra/stacks/openclaw/files/memory-sync.py
Viktor Barzin 1dd8f4e2bf openclaw: native MCP servers + daily claude-memory sync
Wire ha-mcp, context7, and the in-pod playwright sidecar as native
MCP servers on OpenClaw via `mcp set` in the container startup
(ConfigMap-baked mcp.servers gets stripped by `doctor --fix`; CLI-set
entries persist). HA URL pulled from new Vault key
secret/openclaw.ha_sofia_mcp_url and passed via the
HA_SOFIA_MCP_URL env var.

Add a daily 03:00 UTC `memory-sync` CronJob in the openclaw
namespace: pulls all non-sensitive memories from
claude-memory.claude-memory.svc:80/api/memories, groups by category,
writes 18 Markdown files into /workspace/memory/projects/claude-
memory-sync/ (the path memory-core indexes), then triggers
`openclaw memory index --force` via kubectl exec. Reuses the
existing cluster-healthcheck SA (pods+pods/exec). Smoke test: 1488
memories synced, 25/25 files indexed, search returns hits.

Also drops the legacy /app/extensions entry from
plugins.load.paths (doctor warning), wires HA_SOFIA_MCP_URL env,
and one-shot deletes the stale 2026-02-28 metaclaw-export.json from
the openclaw home volume.

claude_memory MCP intentionally NOT wired — its /mcp/mcp transport
404s on the deployed claude-memory-mcp:17 image (tracked as
code-z1so). Shared knowledge is delivered via the CronJob's REST
sync instead. Adding claude_memory to mcp.servers is a one-line
follow-up once that's fixed.
2026-05-22 14:16:53 +00:00

90 lines
3.1 KiB
Python

"""claude-memory → OpenClaw memory-core sync.
Pulls memories from the central claude-memory REST API, writes per-category
Markdown files into /workspace/memory/projects/claude-memory-sync/
which memory-core picks up via its QMD backend.
Runs inside the openclaw pod (piped via `kubectl exec -i -- python3 -`).
Uses MEMORY_API_URL + MEMORY_API_KEY env vars already set on the pod.
Filters out is_sensitive=true memories. Also one-shot deletes the stale
metaclaw-export.json from a prior export attempt.
"""
import json
import os
import pathlib
import sys
import time
import urllib.request
def main() -> int:
api_url = os.environ["MEMORY_API_URL"].rstrip("/")
api_key = os.environ["MEMORY_API_KEY"]
req = urllib.request.Request(
f"{api_url}/api/memories?limit=10000",
headers={"Authorization": f"Bearer {api_key}"},
)
with urllib.request.urlopen(req, timeout=30) as r:
data = json.load(r)
raw = data.get("memories", [])
mems = [m for m in raw if not m.get("is_sensitive", False)]
sensitive_count = len(raw) - len(mems)
by_cat: dict[str, list[dict]] = {}
for m in mems:
by_cat.setdefault(m.get("category") or "uncategorized", []).append(m)
# Write under /workspace/memory/ — memory-core's QMD backend auto-indexes
# this path on every reindex. /home/node/.openclaw/memory/ is the
# SQLite index location, not a content source.
out_dir = pathlib.Path("/workspace/memory/projects/claude-memory-sync")
out_dir.mkdir(parents=True, exist_ok=True)
stamp = time.strftime("%Y-%m-%d %H:%M UTC", time.gmtime())
for cat, items in sorted(by_cat.items()):
items.sort(key=lambda x: x.get("id", 0))
lines = [
f"# {cat.title()} memories",
"",
f"_Synced from claude-memory at {stamp}. {len(items)} memories._",
"",
]
for m in items:
content = m.get("content") or ""
first_line = content.splitlines()[0] if content else ""
title = first_line.lstrip("# ").strip()[:120] or f"#{m['id']}"
lines.extend([
f"## #{m['id']}{title}",
"",
f"- Tags: `{m.get('tags', '')}`",
f"- Importance: {float(m.get('importance', 0.5)):.2f}",
f"- Created: {m.get('created_at', '?')}",
f"- Updated: {m.get('updated_at', '?')}",
"",
content,
"",
"---",
"",
])
(out_dir / f"{cat}.md").write_text("\n".join(lines))
# One-shot: nuke the stale 2026-02-28 export sitting next to memory-core.
stale = pathlib.Path("/home/node/.openclaw/memory/metaclaw-export.json")
if stale.exists():
stale.unlink()
print("[sync] deleted stale metaclaw-export.json")
total = sum(len(v) for v in by_cat.values())
print(
f"[sync] wrote {total} memories across {len(by_cat)} categories to "
f"{out_dir} (skipped {sensitive_count} sensitive)"
)
return 0
if __name__ == "__main__":
sys.exit(main())