fix(mem0-plugin): thread cwd to resolvers, add write-path tests, userConfig for API key
- on_pre_compact.py and capture_compact_summary.py now read cwd from hook stdin and pass it to resolve_project_id/resolve_branch (monorepo safe) - 5 new tests verify all write functions use app_id top-level and never put project_id in metadata - plugin.json declares userConfig.MEM0_API_KEY so Claude Code prompts for the key at install time (sensitive, stored in system keychain)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mem0",
|
"name": "mem0",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows using the Mem0 Platform MCP server.",
|
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows using the Mem0 Platform MCP server.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Mem0",
|
"name": "Mem0",
|
||||||
@@ -9,5 +9,12 @@
|
|||||||
"homepage": "https://mem0.ai",
|
"homepage": "https://mem0.ai",
|
||||||
"repository": "https://github.com/mem0ai/mem0",
|
"repository": "https://github.com/mem0ai/mem0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"keywords": ["memory", "personalization", "mcp", "semantic-search"]
|
"keywords": ["memory", "personalization", "mcp", "semantic-search"],
|
||||||
|
"userConfig": {
|
||||||
|
"MEM0_API_KEY": {
|
||||||
|
"description": "Your Mem0 API key (get one at https://app.mem0.ai/dashboard/api-keys or run: mem0 init --agent --json)",
|
||||||
|
"required": true,
|
||||||
|
"sensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,9 +153,10 @@ def main():
|
|||||||
return
|
return
|
||||||
|
|
||||||
session_id = hook_input.get("session_id", "")
|
session_id = hook_input.get("session_id", "")
|
||||||
|
cwd = hook_input.get("cwd") or None
|
||||||
user_id = resolve_user_id()
|
user_id = resolve_user_id()
|
||||||
project_id = resolve_project_id()
|
project_id = resolve_project_id(cwd)
|
||||||
branch = resolve_branch()
|
branch = resolve_branch(cwd)
|
||||||
|
|
||||||
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
|
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
|
||||||
if not lines:
|
if not lines:
|
||||||
|
|||||||
@@ -234,9 +234,10 @@ def main():
|
|||||||
return
|
return
|
||||||
|
|
||||||
session_id = hook_input.get("session_id", "")
|
session_id = hook_input.get("session_id", "")
|
||||||
|
cwd = hook_input.get("cwd") or None
|
||||||
user_id = resolve_user_id()
|
user_id = resolve_user_id()
|
||||||
project_id = resolve_project_id()
|
project_id = resolve_project_id(cwd)
|
||||||
branch = resolve_branch()
|
branch = resolve_branch(cwd)
|
||||||
|
|
||||||
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
|
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
|
||||||
if not lines:
|
if not lines:
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
"""Tests for write-path app_id migration.
|
||||||
|
|
||||||
|
Verifies that all scripts writing to the Mem0 API:
|
||||||
|
1. Pass app_id as a top-level parameter (not in metadata)
|
||||||
|
2. Do NOT include project_id in metadata
|
||||||
|
3. Include branch in metadata when available
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_import_post_memory_uses_app_id():
|
||||||
|
"""auto_import.post_memory sends app_id top-level, not metadata.project_id."""
|
||||||
|
from auto_import import post_memory
|
||||||
|
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def mock_urlopen(req, timeout=None):
|
||||||
|
body = json.loads(req.data.decode("utf-8"))
|
||||||
|
captured.update(body)
|
||||||
|
resp = MagicMock()
|
||||||
|
resp.status = 200
|
||||||
|
resp.__enter__ = lambda s: s
|
||||||
|
resp.__exit__ = MagicMock(return_value=False)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
with patch("urllib.request.urlopen", side_effect=mock_urlopen):
|
||||||
|
result = post_memory(
|
||||||
|
api_key="test-key",
|
||||||
|
content="test content",
|
||||||
|
user_id="testuser",
|
||||||
|
filename="CLAUDE.md",
|
||||||
|
project_id="my-project",
|
||||||
|
branch="main",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
assert captured["app_id"] == "my-project"
|
||||||
|
assert captured["user_id"] == "testuser"
|
||||||
|
assert "project_id" not in captured.get("metadata", {})
|
||||||
|
assert captured["metadata"]["type"] == "project_profile"
|
||||||
|
assert captured["metadata"]["branch"] == "main"
|
||||||
|
assert captured["infer"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_import_post_memory_omits_empty_branch():
|
||||||
|
"""auto_import.post_memory skips branch in metadata when empty."""
|
||||||
|
from auto_import import post_memory
|
||||||
|
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def mock_urlopen(req, timeout=None):
|
||||||
|
body = json.loads(req.data.decode("utf-8"))
|
||||||
|
captured.update(body)
|
||||||
|
resp = MagicMock()
|
||||||
|
resp.status = 200
|
||||||
|
resp.__enter__ = lambda s: s
|
||||||
|
resp.__exit__ = MagicMock(return_value=False)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
with patch("urllib.request.urlopen", side_effect=mock_urlopen):
|
||||||
|
post_memory("key", "content", "user", "FILE.md", "proj", branch="")
|
||||||
|
|
||||||
|
assert "branch" not in captured.get("metadata", {})
|
||||||
|
|
||||||
|
|
||||||
|
def test_on_pre_compact_store_memory_uses_app_id():
|
||||||
|
"""on_pre_compact.store_memory sends app_id top-level."""
|
||||||
|
from on_pre_compact import store_memory
|
||||||
|
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def mock_urlopen(req, timeout=None):
|
||||||
|
body = json.loads(req.data.decode("utf-8"))
|
||||||
|
captured.update(body)
|
||||||
|
resp = MagicMock()
|
||||||
|
resp.status = 200
|
||||||
|
resp.__enter__ = lambda s: s
|
||||||
|
resp.__exit__ = MagicMock(return_value=False)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
with patch("urllib.request.urlopen", side_effect=mock_urlopen):
|
||||||
|
result = store_memory(
|
||||||
|
api_key="test-key",
|
||||||
|
content="session state content",
|
||||||
|
user_id="testuser",
|
||||||
|
source="pre-compaction",
|
||||||
|
session_id="sess-123",
|
||||||
|
project_id="my-project",
|
||||||
|
branch="feat/auth",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
assert captured["app_id"] == "my-project"
|
||||||
|
assert captured["user_id"] == "testuser"
|
||||||
|
assert "project_id" not in captured.get("metadata", {})
|
||||||
|
assert captured["metadata"]["type"] == "session_state"
|
||||||
|
assert captured["metadata"]["source"] == "pre-compaction"
|
||||||
|
assert captured["metadata"]["branch"] == "feat/auth"
|
||||||
|
assert "expiration_date" in captured
|
||||||
|
|
||||||
|
|
||||||
|
def test_capture_compact_summary_store_uses_app_id():
|
||||||
|
"""capture_compact_summary.store_summary sends app_id top-level."""
|
||||||
|
from capture_compact_summary import store_summary
|
||||||
|
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def mock_urlopen(req, timeout=None):
|
||||||
|
body = json.loads(req.data.decode("utf-8"))
|
||||||
|
captured.update(body)
|
||||||
|
resp = MagicMock()
|
||||||
|
resp.status = 200
|
||||||
|
resp.__enter__ = lambda s: s
|
||||||
|
resp.__exit__ = MagicMock(return_value=False)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
with patch("urllib.request.urlopen", side_effect=mock_urlopen):
|
||||||
|
result = store_summary(
|
||||||
|
api_key="test-key",
|
||||||
|
summary="compact summary text",
|
||||||
|
user_id="testuser",
|
||||||
|
session_id="sess-456",
|
||||||
|
project_id="my-project",
|
||||||
|
branch="main",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
assert captured["app_id"] == "my-project"
|
||||||
|
assert captured["user_id"] == "testuser"
|
||||||
|
assert "project_id" not in captured.get("metadata", {})
|
||||||
|
assert captured["metadata"]["type"] == "compact_summary"
|
||||||
|
assert captured["metadata"]["branch"] == "main"
|
||||||
|
assert captured["infer"] is False
|
||||||
|
assert "expiration_date" in captured
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_metadata_project_id_anywhere():
|
||||||
|
"""Ensure none of the write functions put project_id in metadata."""
|
||||||
|
from auto_import import post_memory
|
||||||
|
from capture_compact_summary import store_summary
|
||||||
|
from on_pre_compact import store_memory
|
||||||
|
|
||||||
|
bodies = []
|
||||||
|
|
||||||
|
def mock_urlopen(req, timeout=None):
|
||||||
|
body = json.loads(req.data.decode("utf-8"))
|
||||||
|
bodies.append(body)
|
||||||
|
resp = MagicMock()
|
||||||
|
resp.status = 200
|
||||||
|
resp.__enter__ = lambda s: s
|
||||||
|
resp.__exit__ = MagicMock(return_value=False)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
with patch("urllib.request.urlopen", side_effect=mock_urlopen):
|
||||||
|
post_memory("k", "c", "u", "f", "proj", "br")
|
||||||
|
store_memory("k", "c", "u", "src", "sid", "proj", "br")
|
||||||
|
store_summary("k", "s", "u", "sid", "proj", "br")
|
||||||
|
|
||||||
|
for i, body in enumerate(bodies):
|
||||||
|
metadata = body.get("metadata", {})
|
||||||
|
assert "project_id" not in metadata, f"Write function #{i} still has metadata.project_id"
|
||||||
|
assert body.get("app_id") == "proj", f"Write function #{i} missing app_id top-level"
|
||||||
Reference in New Issue
Block a user