136 lines
3.8 KiB
Python
136 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Pre-commit memory capture.
|
|
|
|
Captures a summary of staged changes as a mem0 memory before each commit.
|
|
Runs as a background fire-and-forget call — never blocks the commit.
|
|
|
|
Usage:
|
|
git diff --cached --stat | python3 on_pre_commit.py
|
|
# or with full diff:
|
|
git diff --cached | python3 on_pre_commit.py --full
|
|
|
|
Env vars required: MEM0_API_KEY (or CLAUDE_PLUGIN_OPTION_MEM0_API_KEY)
|
|
Env vars optional: MEM0_RESOLVED_USER_ID, MEM0_PROJECT_ID, MEM0_BRANCH
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
from _identity import resolve_api_key, resolve_user_id
|
|
|
|
try:
|
|
from _project import resolve_branch, resolve_project_id
|
|
except ImportError:
|
|
def resolve_project_id() -> str:
|
|
return os.path.basename(os.getcwd())
|
|
|
|
def resolve_branch() -> str:
|
|
return "unknown"
|
|
|
|
|
|
def get_commit_message() -> str:
|
|
"""Read the current commit message from COMMIT_EDITMSG (pre-commit context).
|
|
|
|
Falls back to HEAD's message if COMMIT_EDITMSG doesn't exist yet
|
|
(e.g., when invoked outside the git hook context).
|
|
"""
|
|
try:
|
|
git_dir = subprocess.run(
|
|
["git", "rev-parse", "--git-dir"],
|
|
capture_output=True, text=True, timeout=5,
|
|
)
|
|
if git_dir.returncode == 0:
|
|
editmsg = os.path.join(git_dir.stdout.strip(), "COMMIT_EDITMSG")
|
|
if os.path.isfile(editmsg):
|
|
with open(editmsg) as f:
|
|
first_line = f.readline().strip()
|
|
if first_line and not first_line.startswith("#"):
|
|
return first_line
|
|
except Exception:
|
|
pass
|
|
try:
|
|
result = subprocess.run(
|
|
["git", "log", "-1", "--format=%s", "HEAD"],
|
|
capture_output=True, text=True, timeout=5,
|
|
)
|
|
return result.stdout.strip() if result.returncode == 0 else ""
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
def get_staged_summary() -> str:
|
|
try:
|
|
result = subprocess.run(
|
|
["git", "diff", "--cached", "--stat"],
|
|
capture_output=True, text=True, timeout=10,
|
|
)
|
|
return result.stdout.strip() if result.returncode == 0 else ""
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
def main() -> int:
|
|
api_key = resolve_api_key()
|
|
if not api_key:
|
|
return 0
|
|
|
|
diff_input = sys.stdin.read().strip() if not sys.stdin.isatty() else ""
|
|
staged = diff_input or get_staged_summary()
|
|
if not staged or len(staged) < 10:
|
|
return 0
|
|
|
|
user_id = os.environ.get("MEM0_RESOLVED_USER_ID") or resolve_user_id()
|
|
project_id = os.environ.get("MEM0_PROJECT_ID") or resolve_project_id()
|
|
branch = os.environ.get("MEM0_BRANCH") or resolve_branch()
|
|
commit_msg = get_commit_message()
|
|
|
|
lines = staged.splitlines()
|
|
if len(lines) > 30:
|
|
staged = "\n".join(lines[:30]) + f"\n... ({len(lines) - 30} more lines)"
|
|
|
|
content = f"## Commit Context\n\nBranch: {branch}\n"
|
|
if commit_msg:
|
|
content += f"Message: {commit_msg}\n"
|
|
content += f"\n### Staged Changes\n```\n{staged}\n```"
|
|
|
|
body = json.dumps({
|
|
"messages": [{"role": "user", "content": content}],
|
|
"user_id": user_id,
|
|
"app_id": project_id,
|
|
"metadata": {
|
|
"type": "commit_context",
|
|
"branch": branch,
|
|
"source": "pre-commit",
|
|
},
|
|
"infer": False,
|
|
}).encode()
|
|
|
|
req = urllib.request.Request(
|
|
"https://api.mem0.ai/v3/memories/add/",
|
|
data=body,
|
|
headers={
|
|
"Authorization": f"Token {api_key}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
method="POST",
|
|
)
|
|
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=10):
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|