From cd660507484cafa8e62b2921c2d38c00b29a0965 Mon Sep 17 00:00:00 2001 From: kartik-mem0 Date: Thu, 21 May 2026 18:13:59 +0530 Subject: [PATCH] feat(mem0-plugin): smart retrieval, citations, pre-commit capture (Tier 7) Smart retrieval tier: - on_user_prompt.sh: stack-trace detection (auto-searches anti_patterns on errors) - on_user_prompt.sh: file-aware retrieval (extracts file paths, suggests scoped search) - on_user_prompt.sh: recency boost guidance for state-related queries - mem0-mcp/SKILL.md: inline citation format [mem0:<8char_id>] - on_pre_commit.py: captures staged diff as commit_context memory via REST API --- mem0-plugin/scripts/on_pre_commit.py | 116 ++++++++++++++++++++++++++ mem0-plugin/scripts/on_user_prompt.sh | 33 ++++++++ mem0-plugin/skills/mem0-mcp/SKILL.md | 16 ++++ mem0-plugin/tests/test_pre_commit.py | 54 ++++++++++++ 4 files changed, 219 insertions(+) create mode 100644 mem0-plugin/scripts/on_pre_commit.py create mode 100644 mem0-plugin/tests/test_pre_commit.py diff --git a/mem0-plugin/scripts/on_pre_commit.py b/mem0-plugin/scripts/on_pre_commit.py new file mode 100644 index 000000000..db334a68c --- /dev/null +++ b/mem0-plugin/scripts/on_pre_commit.py @@ -0,0 +1,116 @@ +#!/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: + 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()) diff --git a/mem0-plugin/scripts/on_user_prompt.sh b/mem0-plugin/scripts/on_user_prompt.sh index f34d48994..fdfa86f05 100755 --- a/mem0-plugin/scripts/on_user_prompt.sh +++ b/mem0-plugin/scripts/on_user_prompt.sh @@ -35,6 +35,15 @@ if [ -z "${MEM0_API_KEY:-}" ]; then fi USER_ID="$MEM0_RESOLVED_USER_ID" +# Detect stack traces and error patterns in the prompt +HAS_ERROR="" +if echo "$PROMPT" | grep -qiE '(Traceback|Error:|Exception:|panic:|FAILED|fatal:| at .+\.[a-z]+:[0-9]+)'; then + HAS_ERROR="true" +fi + +# Detect file paths in the prompt +FILE_PATHS=$(echo "$PROMPT" | grep -oE '([a-zA-Z0-9_./-]+\.(py|ts|tsx|js|jsx|rs|go|rb|java|sh|yaml|yml|json|toml|md|sql|css|html))\b' 2>/dev/null | head -5 || echo "") + cat <"}}\`. Skip recency for durable facts (conventions, decisions). - Empty results are normal -- proceed without context. EOF diff --git a/mem0-plugin/skills/mem0-mcp/SKILL.md b/mem0-plugin/skills/mem0-mcp/SKILL.md index a5b2e334d..3bde9c7a0 100644 --- a/mem0-plugin/skills/mem0-mcp/SKILL.md +++ b/mem0-plugin/skills/mem0-mcp/SKILL.md @@ -206,6 +206,22 @@ If context is about to be compacted or the session is ending, store a comprehens Include metadata: `{"type": "session_state"}` +## Inline citations + +When your response is informed by specific memories, cite them so the user can trace provenance. Use the memory ID returned by `search_memories`. + +Format: `[mem0:]` where `` is the first 8 characters of the memory ID. + +Example: +> We chose Postgres over SQLite for production [mem0:a3f8b2c1] and the auth module uses JWT tokens [mem0:7e2d9f4a]. + +Rules: +- Only cite when the memory **directly informed** your answer. Don't cite for general knowledge. +- Place citations inline, at the end of the relevant sentence. +- If multiple memories support the same point, cite all: `[mem0:abc12345][mem0:def67890]`. +- Don't cite `session_state` or `compact_summary` memories — those are internal bookkeeping. +- Keep it subtle. One or two citations per response is typical. Don't over-cite. + ## Memory hygiene - Do NOT write to MEMORY.md or any file-based memory. Use mem0 MCP tools exclusively. diff --git a/mem0-plugin/tests/test_pre_commit.py b/mem0-plugin/tests/test_pre_commit.py new file mode 100644 index 000000000..5757778b8 --- /dev/null +++ b/mem0-plugin/tests/test_pre_commit.py @@ -0,0 +1,54 @@ +"""Tests for on_pre_commit.py — pre-commit memory capture.""" + +from __future__ import annotations + +import os +from unittest.mock import MagicMock + +SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "scripts") + + +def test_import_succeeds(): + """on_pre_commit module can be imported.""" + import on_pre_commit + + assert hasattr(on_pre_commit, "main") + + +def test_no_api_key_exits_zero(monkeypatch): + """main() exits 0 when no API key is set.""" + import on_pre_commit + + monkeypatch.delenv("MEM0_API_KEY", raising=False) + monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", raising=False) + monkeypatch.setattr("sys.stdin", MagicMock(isatty=lambda: True)) + + assert on_pre_commit.main() == 0 + + +def test_empty_diff_exits_zero(monkeypatch): + """main() exits 0 when stdin diff is empty.""" + from io import StringIO + + import on_pre_commit + + monkeypatch.setenv("MEM0_API_KEY", "test-key") + monkeypatch.setattr("sys.stdin", StringIO("")) + + assert on_pre_commit.main() == 0 + + +def test_get_staged_summary_runs(): + """get_staged_summary doesn't crash even outside a git repo.""" + import on_pre_commit + + result = on_pre_commit.get_staged_summary() + assert isinstance(result, str) + + +def test_get_commit_message_runs(): + """get_commit_message doesn't crash even outside a git repo.""" + import on_pre_commit + + result = on_pre_commit.get_commit_message() + assert isinstance(result, str)