#!/usr/bin/env python3 """Auto-import declarative project files into mem0. Runs in the background from the SessionStart hook (startup only). Imports CLAUDE.md, AGENTS.md, .cursorrules, .windsurfrules, mem0.md into mem0 as project profile memories, skipping unchanged files via SHA-256 hashing. Input: MEM0_CWD env var (optional, defaults to os.getcwd()) Output: stderr logs only (exit 0 always — must not block) """ from __future__ import annotations import hashlib import json import logging import os import sys import urllib.error import urllib.request sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _identity import resolve_user_id from _project import resolve_project_id log = logging.getLogger("mem0-auto-import") log.setLevel(logging.DEBUG) _handler = logging.StreamHandler(sys.stderr) _handler.setFormatter(logging.Formatter("[mem0-auto-import] %(message)s")) log.addHandler(_handler) if os.environ.get("MEM0_DEBUG"): _log_dir = os.path.expanduser("~/.mem0") try: os.makedirs(_log_dir, exist_ok=True) _file_handler = logging.FileHandler(os.path.join(_log_dir, "hooks.log")) _file_handler.setFormatter(logging.Formatter("[mem0-auto-import] %(asctime)s %(message)s")) log.addHandler(_file_handler) except OSError: pass API_URL = "https://api.mem0.ai" MAX_FILE_SIZE = 100_000 # skip files over 100 KB TARGET_FILES = ["CLAUDE.md", "AGENTS.md", ".cursorrules", ".windsurfrules", "mem0.md"] HASH_STORE = os.path.expanduser("~/.mem0/file_hashes.json") def sha256_file(path: str) -> str: """Return the hex SHA-256 digest of a file.""" h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(65536), b""): h.update(chunk) return h.hexdigest() def load_hashes() -> dict[str, str]: """Load the hash store from disk; return empty dict on any error.""" if not os.path.isfile(HASH_STORE): return {} try: with open(HASH_STORE) as f: return json.load(f) except (OSError, json.JSONDecodeError): return {} def save_hashes(hashes: dict[str, str]) -> None: """Persist the hash store to disk.""" mem0_dir = os.path.expanduser("~/.mem0") os.makedirs(mem0_dir, exist_ok=True) try: with open(HASH_STORE, "w") as f: json.dump(hashes, f, indent=2) except OSError as e: log.warning("Could not save hash store: %s", e) def post_memory(api_key: str, content: str, user_id: str, filename: str, project_id: str) -> bool: """POST a project profile memory to the Mem0 REST API.""" body = { "messages": [ { "role": "user", "content": f"## Project Profile: {filename}\n\nProject: {project_id}\n\n{content}", } ], "user_id": user_id, "metadata": { "type": "project_profile", "file": filename, "project_id": project_id, "source": "auto-import", }, "infer": False, } data = json.dumps(body).encode("utf-8") req = urllib.request.Request( f"{API_URL}/v1/memories/", data=data, headers={ "Content-Type": "application/json", "Authorization": f"Token {api_key}", }, method="POST", ) try: with urllib.request.urlopen(req, timeout=15) as resp: if resp.status in (200, 201): log.info("Imported %s (project=%s)", filename, project_id) return True log.warning("API returned status %d for %s", resp.status, filename) return False except urllib.error.URLError as e: log.warning("API call failed for %s: %s", filename, e) return False def main() -> None: api_key = os.environ.get("MEM0_API_KEY", "") if not api_key: log.debug("MEM0_API_KEY not set, skipping auto-import") return cwd = os.environ.get("MEM0_CWD", "").strip() or os.getcwd() user_id = resolve_user_id() project_id = resolve_project_id(cwd) log.debug("Auto-import started: cwd=%s project=%s user=%s", cwd, project_id, user_id) hashes = load_hashes() updated = False for filename in TARGET_FILES: filepath = os.path.join(cwd, filename) if not os.path.isfile(filepath): log.debug("Not found, skipping: %s", filename) continue try: file_size = os.path.getsize(filepath) except OSError: log.debug("Cannot stat %s, skipping", filename) continue if file_size > MAX_FILE_SIZE: log.debug("Skipping %s: size %d exceeds %d bytes", filename, file_size, MAX_FILE_SIZE) continue try: current_hash = sha256_file(filepath) except OSError as e: log.debug("Cannot hash %s: %s", filename, e) continue hash_key = f"{project_id}:{filename}" if hashes.get(hash_key) == current_hash: log.debug("Unchanged, skipping: %s", filename) continue try: with open(filepath, encoding="utf-8", errors="replace") as f: content = f.read() except OSError as e: log.debug("Cannot read %s: %s", filename, e) continue if post_memory(api_key, content, user_id, filename, project_id): hashes[hash_key] = current_hash updated = True # on API failure we don't update the hash — retry next session if updated: save_hashes(hashes) else: log.debug("No files imported this run") if __name__ == "__main__": try: main() except Exception as e: log.error("Unexpected error: %s", e) sys.exit(0)