fix(mem0-plugin): v0.2.2 (#5234)

This commit is contained in:
Kartik
2026-05-22 21:47:14 +05:30
committed by GitHub
parent 53a3998873
commit 16a7702d09
41 changed files with 889 additions and 675 deletions
+75
View File
@@ -0,0 +1,75 @@
"""Shared content-chunking utilities for mem0-plugin import scripts."""
from __future__ import annotations
MIN_CHUNK_CHARS = 50
MAX_CHUNK_CHARS = 10_000
def split_by_headers(content: str, header_prefix: str = "## ") -> list[str]:
"""Split content by Markdown header lines (e.g. '## ').
The header line is included at the start of each chunk.
Returns a list of non-empty chunk strings.
"""
chunks: list[str] = []
current_lines: list[str] = []
for line in content.splitlines(keepends=True):
if line.startswith(header_prefix) and current_lines:
chunk = "".join(current_lines).strip()
if chunk:
chunks.append(chunk)
current_lines = [line]
else:
current_lines.append(line)
if current_lines:
chunk = "".join(current_lines).strip()
if chunk:
chunks.append(chunk)
return chunks
def split_by_hr_or_headers(content: str) -> list[str]:
"""Split content by '---' horizontal rules or '## ' headers.
Used for .continue/rules.md which may use either convention.
"""
import re
# Split on lines that are exactly "---" or start with "## "
chunks: list[str] = []
current_lines: list[str] = []
for line in content.splitlines(keepends=True):
is_hr = re.match(r"^---\s*$", line)
is_h2 = line.startswith("## ")
if (is_hr or is_h2) and current_lines:
chunk = "".join(current_lines).strip()
if chunk:
chunks.append(chunk)
current_lines = [] if is_hr else [line]
else:
current_lines.append(line)
if current_lines:
chunk = "".join(current_lines).strip()
if chunk:
chunks.append(chunk)
return chunks
def filter_and_truncate(chunks: list[str]) -> list[str]:
"""Filter out chunks shorter than MIN_CHUNK_CHARS, truncate long chunks."""
result: list[str] = []
for chunk in chunks:
if len(chunk) < MIN_CHUNK_CHARS:
continue
if len(chunk) > MAX_CHUNK_CHARS:
chunk = chunk[:MAX_CHUNK_CHARS]
result.append(chunk)
return result
+46 -1
View File
@@ -3,6 +3,7 @@
Resolution priority (project_id):
1. MEM0_PROJECT_ID env var (explicit override)
2. ~/.mem0/project_map.json lookup by cwd
2b. ~/.mem0/project_map.json lookup by remote hash (self-healing fallback)
3. Git remote slug: strip protocol/prefix, strip .git, replace / and : with -
e.g. git@github.com:mem0ai/mem0.git -> mem0ai-mem0
4. Fallback: basename of cwd
@@ -10,6 +11,7 @@ Resolution priority (project_id):
from __future__ import annotations
import hashlib
import json
import os
import re
@@ -34,6 +36,19 @@ def resolve_project_id(cwd: str | None = None) -> str:
mapped = project_map.get(cwd, "").strip()
if mapped:
return mapped
# 2b. Remote hash fallback (self-healing when folder is moved/renamed)
remote_key = _remote_hash_key(cwd)
if remote_key:
mapped = project_map.get(remote_key, "").strip()
if mapped:
# Self-heal: write the new CWD key so future lookups are fast
project_map[cwd] = mapped
try:
with open(map_path, "w") as f:
json.dump(project_map, f, indent=2)
except OSError:
pass
return mapped
except (OSError, json.JSONDecodeError, AttributeError):
pass
@@ -76,7 +91,7 @@ def resolve_branch(cwd: str | None = None) -> str:
def save_project_mapping(cwd: str, project_id: str) -> None:
"""Write cwd -> project_id into ~/.mem0/project_map.json."""
"""Write cwd -> project_id (and remote hash key -> project_id) into ~/.mem0/project_map.json."""
mem0_dir = os.path.expanduser("~/.mem0")
os.makedirs(mem0_dir, exist_ok=True)
map_path = os.path.join(mem0_dir, "project_map.json")
@@ -88,10 +103,40 @@ def save_project_mapping(cwd: str, project_id: str) -> None:
except (OSError, json.JSONDecodeError):
project_map = {}
project_map[cwd] = project_id
# Also write the remote hash key so the mapping survives folder moves/renames
remote_key = _remote_hash_key(cwd)
if remote_key:
project_map[remote_key] = project_id
with open(map_path, "w") as f:
json.dump(project_map, f, indent=2)
def _remote_hash_key(cwd: str | None = None) -> str:
"""Return a stable key derived from the git remote URL.
Runs ``git config --get remote.origin.url`` in *cwd* and returns a string
of the form ``remote:<sha256(url)[:16]>``. Returns an empty string when
the directory is not a git repo or has no remote configured.
"""
if cwd is None:
cwd = os.getcwd()
try:
result = subprocess.run(
["git", "config", "--get", "remote.origin.url"],
capture_output=True,
text=True,
check=True,
cwd=cwd,
)
url = result.stdout.strip()
if not url:
return ""
digest = hashlib.sha256(url.encode()).hexdigest()[:16]
return f"remote:{digest}"
except (subprocess.CalledProcessError, OSError):
return ""
def _remote_url_to_slug(url: str) -> str:
"""Convert a git remote URL to a deterministic slug.
+17 -2
View File
@@ -21,6 +21,7 @@ import urllib.error
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _chunking import filter_and_truncate, split_by_headers
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id
@@ -198,10 +199,24 @@ def main() -> None:
log.debug("Cannot read %s: %s", filename, e)
continue
if post_memory(api_key, content, user_id, filename, project_id, branch):
is_markdown = filename.endswith(".md")
if is_markdown:
chunks = filter_and_truncate(split_by_headers(content))
else:
chunks = filter_and_truncate([content])
if not chunks:
chunks = [content[:10000]]
success = True
for i, chunk in enumerate(chunks):
chunk_name = f"{filename}[{i+1}/{len(chunks)}]" if len(chunks) > 1 else filename
if not post_memory(api_key, chunk, user_id, chunk_name, project_id, branch):
success = False
if success:
hashes[hash_key] = current_hash
updated = True
# on API failure we don't update the hash — retry next session
if updated:
save_hashes(hashes)
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# PreToolUse hook for mcp__mem0__add_memory.
# Injects default metadata fields (confidence, files, source, type) when the
# agent omits them. Uses the hookSpecificOutput.updatedInput contract to
# actually modify the tool call parameters.
#
# Hook contract:
# exit 0 = allow. If stdout contains {"hookSpecificOutput": {"updatedInput": ...}},
# the updatedInput replaces the tool's input parameters.
# exit 2 = block (stderr shown as rejection reason).
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
case "$TOOL_NAME" in
mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory) ;;
*) exit 0 ;;
esac
TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input // "{}"' 2>/dev/null)
PATCHED=$(python3 -c "
import json, sys
raw = sys.stdin.read()
try:
inp = json.loads(raw)
except Exception:
sys.exit(0)
meta = inp.get('metadata') or {}
changed = False
if 'confidence' not in meta:
meta['confidence'] = 0.7
changed = True
if 'files' not in meta:
meta['files'] = ['*']
changed = True
if 'source' not in meta:
meta['source'] = 'auto_capture'
changed = True
if 'type' not in meta:
meta['type'] = 'task_learning'
changed = True
if changed:
inp['metadata'] = meta
print(json.dumps(inp))
" <<< "$TOOL_INPUT" 2>/dev/null || true)
if [ -n "$PATCHED" ]; then
# Use hookSpecificOutput.updatedInput to actually modify the tool call
jq -n --argjson updated "$PATCHED" '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": $updated
}
}'
fi
exit 0
+25 -7
View File
@@ -11,6 +11,8 @@ REQ_STAMP="${DATA_DIR}/requirements.txt"
mkdir -p "${DATA_DIR}"
LOCKDIR="${DATA_DIR}/.install-lock"
needs_install=false
if [ ! -f "${VENV_DIR}/bin/python3" ]; then
@@ -20,13 +22,29 @@ elif ! diff -q "${REQ_SRC}" "${REQ_STAMP}" >/dev/null 2>&1; then
fi
if [ "${needs_install}" = "true" ]; then
python3 -m venv "${VENV_DIR}" 2>/dev/null || python -m venv "${VENV_DIR}"
"${VENV_DIR}/bin/pip" install --quiet --upgrade pip >/dev/null 2>&1 || true
if "${VENV_DIR}/bin/pip" install --quiet -r "${REQ_SRC}" 2>/dev/null; then
cp "${REQ_SRC}" "${REQ_STAMP}"
if mkdir "${LOCKDIR}" 2>/dev/null; then
# We acquired the lock — proceed with installation
trap 'rmdir "${LOCKDIR}" 2>/dev/null || true' EXIT
python3 -m venv "${VENV_DIR}" 2>/dev/null || python -m venv "${VENV_DIR}"
"${VENV_DIR}/bin/pip" install --quiet --upgrade pip >/dev/null 2>&1 || true
if "${VENV_DIR}/bin/pip" install --quiet -r "${REQ_SRC}" 2>/dev/null; then
cp "${REQ_SRC}" "${REQ_STAMP}"
rm -f "${DATA_DIR}/.install-failed"
else
rm -f "${REQ_STAMP}"
touch "${DATA_DIR}/.install-failed"
echo "mem0 plugin: failed to install Python dependencies" >&2
exit 0
fi
else
rm -f "${REQ_STAMP}"
echo "mem0 plugin: failed to install Python dependencies" >&2
exit 0
# Another process holds the lock — wait up to 60s for it to finish
for i in $(seq 1 60); do
[ ! -d "${LOCKDIR}" ] && break
sleep 1
done
# Check if the other process's install failed
if [ -f "${DATA_DIR}/.install-failed" ]; then
echo "mem0 plugin: dependency installation failed (by another session)" >&2
fi
fi
fi
+5 -77
View File
@@ -24,87 +24,15 @@ import urllib.error
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _chunking import (
filter_and_truncate,
split_by_headers,
split_by_hr_or_headers,
)
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id
API_URL = "https://api.mem0.ai"
MIN_CHUNK_CHARS = 50
MAX_CHUNK_CHARS = 10_000
# ---------------------------------------------------------------------------
# Content splitting utilities
# ---------------------------------------------------------------------------
def split_by_headers(content: str, header_prefix: str = "## ") -> list[str]:
"""Split content by Markdown header lines (e.g. '## ').
The header line is included at the start of each chunk.
Returns a list of non-empty chunk strings.
"""
chunks: list[str] = []
current_lines: list[str] = []
for line in content.splitlines(keepends=True):
if line.startswith(header_prefix) and current_lines:
chunk = "".join(current_lines).strip()
if chunk:
chunks.append(chunk)
current_lines = [line]
else:
current_lines.append(line)
if current_lines:
chunk = "".join(current_lines).strip()
if chunk:
chunks.append(chunk)
return chunks
def split_by_hr_or_headers(content: str) -> list[str]:
"""Split content by '---' horizontal rules or '## ' headers.
Used for .continue/rules.md which may use either convention.
"""
import re
# Split on lines that are exactly "---" or start with "## "
chunks: list[str] = []
current_lines: list[str] = []
for line in content.splitlines(keepends=True):
is_hr = re.match(r"^---\s*$", line)
is_h2 = line.startswith("## ")
if (is_hr or is_h2) and current_lines:
chunk = "".join(current_lines).strip()
if chunk:
chunks.append(chunk)
current_lines = [] if is_hr else [line]
else:
current_lines.append(line)
if current_lines:
chunk = "".join(current_lines).strip()
if chunk:
chunks.append(chunk)
return chunks
def filter_and_truncate(chunks: list[str]) -> list[str]:
"""Filter out chunks shorter than MIN_CHUNK_CHARS, truncate long chunks."""
result: list[str] = []
for chunk in chunks:
if len(chunk) < MIN_CHUNK_CHARS:
continue
if len(chunk) > MAX_CHUNK_CHARS:
chunk = chunk[:MAX_CHUNK_CHARS]
result.append(chunk)
return result
# ---------------------------------------------------------------------------
# API helpers
+3 -1
View File
@@ -33,7 +33,9 @@ esac
# Detect stack traces and error patterns in command output
HAS_ERROR=""
if echo "$TOOL_RESULT" | grep -qiE '(Traceback \(most recent|Error:|Exception:|panic:|FAILED|fatal:|FAIL:| at .+\.[a-z]+:[0-9]+|error\[E[0-9]+\])'; then
if echo "$TOOL_RESULT" | grep -qE '(Traceback \(most recent call last\)|panic: |FATAL:|error\[E[0-9]+\])'; then
HAS_ERROR="true"
elif [ "$(echo "$TOOL_RESULT" | grep -cE '(Error:|Exception:)')" -ge 2 ]; then
HAS_ERROR="true"
fi
+2 -3
View File
@@ -1,9 +1,8 @@
#!/usr/bin/env bash
# Hook: PreToolUse (matcher: Bash)
#
# Detects `git commit` commands and:
# 1. Fires on_pre_commit.py in the background to capture staged changes as memory
# 2. Searches for relevant memories about the changed files and surfaces them
# Detects `git commit` commands and searches for relevant memories
# about the changed files, surfacing them as pre-commit context.
#
# Input: JSON on stdin with tool_name, tool_input
# Output: JSON with additionalContext (relevant memories for the commit)
+8 -1
View File
@@ -44,8 +44,15 @@ if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
# C5: commit prompts default OFF — only prompt if explicitly enabled in mem0.md
MEM0_CWD=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")
COMMIT_PROMPTS=$(python3 "$SCRIPT_DIR/parse_mem0_config.py" --key settings.commit_prompts "$MEM0_CWD" 2>/dev/null || echo "")
if [ "$COMMIT_PROMPTS" != "true" ]; then
exit 0
fi
# Extract changed files from commit
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD 2>/dev/null | head -10 || echo "")
CHANGED_FILES=$(git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null | head -10 || echo "")
if [ -z "$CHANGED_FILES" ]; then
exit 0
fi
-135
View File
@@ -1,135 +0,0 @@
#!/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())
+34 -3
View File
@@ -41,6 +41,15 @@ if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
# Check for failed dependency installation and warn the user
_DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.mem0/plugin-data}"
if [ -f "${_DATA_DIR}/.install-failed" ]; then
echo ""
echo "⚠️ mem0 SDK installation failed. Some features may not work."
echo "Run: ${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}/scripts/ensure_deps.sh"
echo ""
fi
# Fetch project-scoped memory count (best-effort, don't block on failure, 5s timeout)
MEM0_COUNT="?"
if command -v python3 >/dev/null 2>&1; then
@@ -96,10 +105,14 @@ if command -v python3 >/dev/null 2>&1; then
MEM0_PROJECT_CONFIG=$(python3 "$SCRIPT_DIR/parse_mem0_config.py" --full "$MEM0_CWD_RESOLVED" 2>/dev/null || echo "{}")
fi
if [ -n "$MEM0_PROJECT_CONFIG" ] && [ "$MEM0_PROJECT_CONFIG" != "{}" ]; then
_CONFIG_KEYS=$(echo "$MEM0_PROJECT_CONFIG" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d))" 2>/dev/null || echo "?")
echo "### Project Config (mem0.md)"
echo "\`\`\`json"
echo "$MEM0_PROJECT_CONFIG"
echo "\`\`\`"
echo "\`mem0.md loaded (${_CONFIG_KEYS} sections configured)\`"
if [ -n "${MEM0_DEBUG:-}" ]; then
echo "\`\`\`json"
echo "$MEM0_PROJECT_CONFIG"
echo "\`\`\`"
fi
echo ""
fi
@@ -108,6 +121,8 @@ if [ "$SOURCE" = "startup" ]; then
_SAFE_PID=$(printf '%s' "$MEM0_PROJECT_ID" | tr '/:' '--')
_ONBOARD_MARKER="$HOME/.mem0/.onboarded_${_SAFE_PID}"
if [ ! -f "$_ONBOARD_MARKER" ]; then
mkdir -p "$HOME/.mem0" 2>/dev/null || true
touch "$_ONBOARD_MARKER"
cat <<'EOF'
## Mem0 First Run — Automatic Onboarding
@@ -135,6 +150,22 @@ IMPORTANT: Do NOT skip this step. Always bootstrap context first.
EOF
fi
# Detect native Claude Code auto-memory for THIS project
_PROJ_KEY=$(printf '%s' "$MEM0_CWD_RESOLVED" | tr '/' '-')
_MEMORY_MD="$HOME/.claude/projects/${_PROJ_KEY}/memory/MEMORY.md"
if [ -f "$_MEMORY_MD" ] && [ -s "$_MEMORY_MD" ]; then
cat <<MEMEOF
### Native auto-memory detected
Found \`$_MEMORY_MD\`. The mem0 plugin handles all memory storage.
To avoid two parallel memory systems:
- Add \`"autoMemoryEnabled": false\` to \`~/.claude/settings.json\`
- Or run \`/mem0:import\` to migrate existing MEMORY.md content into mem0
MEMEOF
fi
# Auto-import declarative project files in background
MEM0_CWD="$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")" \
python3 "$SCRIPT_DIR/auto_import.py" 2>/dev/null &
+9 -1
View File
@@ -24,9 +24,17 @@ if [ ${#RESULT_SUMMARY} -lt 50 ]; then
exit 0
fi
# Read subagent skip list from config, default to Explore|Plan
_SKIP_CSV=$(python3 "$SCRIPT_DIR/parse_mem0_config.py" --key settings.subagent_skip "$(git rev-parse --show-toplevel 2>/dev/null || echo ".")" 2>/dev/null || echo "")
if [ -n "$_SKIP_CSV" ]; then
_SKIP_PATTERN=$(echo "$_SKIP_CSV" | tr -d ' ' | tr ',' '|')
else
_SKIP_PATTERN="Explore|Plan"
fi
# Skip explorer/plan agents — read-only, rarely produce storable learnings
case "$AGENT_TYPE" in
Explore|Plan)
$_SKIP_PATTERN)
exit 0
;;
esac
+5 -1
View File
@@ -31,7 +31,11 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Detect stack traces and error patterns in the prompt (no API needed)
HAS_ERROR=""
if echo "$PROMPT" | grep -qiE '(Traceback|Error:|Exception:|panic:|FAILED|fatal:| at .+\.[a-z]+:[0-9]+)'; then
if echo "$PROMPT" | grep -qE '(Traceback|panic:)'; then
HAS_ERROR="true"
elif echo "$PROMPT" | grep -qE '^\s*fatal: '; then
HAS_ERROR="true"
elif [ "$(echo "$PROMPT" | grep -cE '(Error:|Exception:|FAIL:)')" -ge 2 ]; then
HAS_ERROR="true"
fi
+40 -2
View File
@@ -199,6 +199,10 @@ def load_full_config(cwd: str | None = None) -> dict:
if ignore:
config["ignore"] = ignore
settings = parse_section_kv(content, "Settings")
if settings:
config["settings"] = settings
return config
@@ -229,10 +233,44 @@ def main() -> int:
With ``--full``, prints the complete config. Without it, prints only
retention policies (backward-compatible).
With ``--key <dotted.path>``, prints the scalar value at that path in the
full config (e.g. ``--key settings.commit_prompts``). Prints an empty
string when the key is absent. Exits 1 only on unexpected errors.
"""
full_mode = "--full" in sys.argv
args = [a for a in sys.argv[1:] if not a.startswith("--")]
cwd = args[0] if args else os.getcwd()
# Extract --key <value>
key_path: str | None = None
raw_args = sys.argv[1:]
filtered_args: list[str] = []
i = 0
while i < len(raw_args):
if raw_args[i] == "--key" and i + 1 < len(raw_args):
key_path = raw_args[i + 1]
i += 2
elif raw_args[i].startswith("--key="):
key_path = raw_args[i][len("--key="):]
i += 1
elif raw_args[i].startswith("--"):
i += 1 # skip other flags like --full
else:
filtered_args.append(raw_args[i])
i += 1
cwd = filtered_args[0] if filtered_args else os.getcwd()
if key_path is not None:
config = load_full_config(cwd)
# Traverse dotted path
value: object = config
for part in key_path.split("."):
if not isinstance(value, dict):
value = None
break
value = value.get(part)
print(value if value is not None else "")
return 0
if full_mode:
config = load_full_config(cwd)
+13 -4
View File
@@ -27,7 +27,16 @@ import sys
import urllib.error
import urllib.request
PLUGIN_VERSION = "0.2.1"
def _load_plugin_version() -> str:
try:
plugin_json = os.path.join(os.path.dirname(__file__), "..", ".claude-plugin", "plugin.json")
with open(plugin_json) as f:
return json.load(f).get("version", "unknown")
except (OSError, json.JSONDecodeError, KeyError):
return "unknown"
PLUGIN_VERSION = _load_plugin_version()
POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/"
@@ -42,10 +51,10 @@ def _sha256(value: str) -> str:
def _distinct_id() -> str:
"""Stable anonymous ID: MD5 of API key if available, else SHA-256 of username."""
"""Stable anonymous ID: SHA-256 of API key if available, else SHA-256 of username."""
api_key = os.environ.get("MEM0_API_KEY") or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY") or ""
if api_key:
return hashlib.md5(api_key.encode()).hexdigest()
return hashlib.sha256(api_key.encode()).hexdigest()[:32]
user_id = os.environ.get("MEM0_RESOLVED_USER_ID") or os.environ.get("USER") or "unknown"
return _sha256(user_id)
@@ -75,6 +84,7 @@ def build_posthog_payload(event_name: str, properties: dict | None = None) -> di
"distinct_id": _distinct_id(),
"event": event_name,
"properties": {
**(properties or {}),
"source": "plugin",
"platform": detect_platform(),
"plugin_version": PLUGIN_VERSION,
@@ -84,7 +94,6 @@ def build_posthog_payload(event_name: str, properties: dict | None = None) -> di
"sample_rate": SAMPLE_RATE,
"$process_person_profile": False,
"$lib": "posthog-python",
**(properties or {}),
},
}