From a67b00d7c72852c97691676c66fbaa5015007525 Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:19:53 -0300 Subject: [PATCH 01/38] perf: cache ChromaDB PersistentClient instead of re-instantiating per call The MCP server previously created a new PersistentClient on every tool call via _get_collection(). This incurs HNSW index loading overhead on each request. Cache the client and collection at module level. The cache resets naturally on process restart (MCP runs as a subprocess). Also adds a _reset_mcp_cache fixture to conftest.py for test isolation. Includes test infrastructure from PR #131. 92 tests pass. --- mempalace/mcp_server.py | 16 ++++++++++++---- tests/conftest.py | 13 +++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/mempalace/mcp_server.py b/mempalace/mcp_server.py index 2169255..81c9b50 100644 --- a/mempalace/mcp_server.py +++ b/mempalace/mcp_server.py @@ -39,13 +39,21 @@ logger = logging.getLogger("mempalace_mcp") _config = MempalaceConfig() +_client_cache = None +_collection_cache = None + + def _get_collection(create=False): - """Return the ChromaDB collection, or None on failure.""" + """Return the ChromaDB collection, caching the client between calls.""" + global _client_cache, _collection_cache try: - client = chromadb.PersistentClient(path=_config.palace_path) + if _client_cache is None: + _client_cache = chromadb.PersistentClient(path=_config.palace_path) if create: - return client.get_or_create_collection(_config.collection_name) - return client.get_collection(_config.collection_name) + _collection_cache = _client_cache.get_or_create_collection(_config.collection_name) + elif _collection_cache is None: + _collection_cache = _client_cache.get_collection(_config.collection_name) + return _collection_cache except Exception: return None diff --git a/tests/conftest.py b/tests/conftest.py index 22b5e42..fa08802 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -34,6 +34,19 @@ from mempalace.config import MempalaceConfig # noqa: E402 from mempalace.knowledge_graph import KnowledgeGraph # noqa: E402 +@pytest.fixture(autouse=True) +def _reset_mcp_cache(): + """Reset the MCP server's cached ChromaDB client/collection between tests.""" + yield + try: + from mempalace import mcp_server + + mcp_server._client_cache = None + mcp_server._collection_cache = None + except (ImportError, AttributeError): + pass + + @pytest.fixture(scope="session", autouse=True) def _isolate_home(): """Ensure HOME points to a temp dir for the entire test session. From 47696bef8c28adae9288af792468923bcb45adf5 Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:29:12 -0300 Subject: [PATCH 02/38] =?UTF-8?q?fix:=20address=20Copilot=20review=20?= =?UTF-8?q?=E2=80=94=20derive=20MCP=20version,=20improve=20test=20isolatio?= =?UTF-8?q?n=20and=20portability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 19 ++++++++++++------- tests/test_mcp_server.py | 7 ++++--- tests/test_searcher.py | 4 ++-- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index fa08802..eb2b432 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,14 +37,19 @@ from mempalace.knowledge_graph import KnowledgeGraph # noqa: E402 @pytest.fixture(autouse=True) def _reset_mcp_cache(): """Reset the MCP server's cached ChromaDB client/collection between tests.""" - yield - try: - from mempalace import mcp_server - mcp_server._client_cache = None - mcp_server._collection_cache = None - except (ImportError, AttributeError): - pass + def _clear_cache(): + try: + from mempalace import mcp_server + + mcp_server._client_cache = None + mcp_server._collection_cache = None + except (ImportError, AttributeError): + pass + + _clear_cache() + yield + _clear_cache() @pytest.fixture(scope="session", autouse=True) diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index cf37a27..09a3c46 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -151,9 +151,10 @@ class TestReadTools: assert result["taxonomy"]["project"]["frontend"] == 1 assert result["taxonomy"]["notes"]["planning"] == 1 - def test_no_palace_returns_error(self, monkeypatch, config, kg): - config._file_config["palace_path"] = "/nonexistent/path" - _patch_mcp_server(monkeypatch, config, "/nonexistent/path", kg) + def test_no_palace_returns_error(self, monkeypatch, config, kg, tmp_path): + missing = str(tmp_path / "missing") + config._file_config["palace_path"] = missing + _patch_mcp_server(monkeypatch, config, missing, kg) from mempalace.mcp_server import tool_status result = tool_status() diff --git a/tests/test_searcher.py b/tests/test_searcher.py index 44a05aa..1c2687d 100644 --- a/tests/test_searcher.py +++ b/tests/test_searcher.py @@ -30,8 +30,8 @@ class TestSearchMemories: result = search_memories("code", palace_path, n_results=2) assert len(result["results"]) <= 2 - def test_no_palace_returns_error(self): - result = search_memories("anything", "/nonexistent/path") + def test_no_palace_returns_error(self, tmp_path): + result = search_memories("anything", str(tmp_path / "missing")) assert "error" in result def test_result_fields(self, palace_path, seeded_collection): From df33550945c158906925c3ad7b90d1f1e1f1c710 Mon Sep 17 00:00:00 2001 From: marerem Date: Wed, 8 Apr 2026 12:43:09 +0200 Subject: [PATCH 03/38] fix: silence ChromaDB telemetry warnings and CoreML segfault on Apple Silicon ChromaDB 0.6.x bundles a Posthog telemetry client whose capture() signature is incompatible with the installed posthog library, producing noisy "Failed to send telemetry event" stderr warnings on every operation. Silence by raising the logger threshold to CRITICAL. ONNX Runtime's CoreML execution provider segfaults during vector queries on macOS ARM64 (issue #74). Auto-set ORT_DISABLE_COREML=1 on Apple Silicon to force CPU execution, while respecting any user-provided override via os.environ.setdefault(). Made-with: Cursor --- mempalace/__init__.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/mempalace/__init__.py b/mempalace/__init__.py index d7a138d..78d760b 100644 --- a/mempalace/__init__.py +++ b/mempalace/__init__.py @@ -1,6 +1,21 @@ """MemPalace — Give your AI a memory. No API key required.""" -from .cli import main -from .version import __version__ +import logging +import os +import platform + +from .cli import main # noqa: E402 +from .version import __version__ # noqa: E402 + +# ChromaDB 0.6.x ships a Posthog telemetry client whose capture() signature is +# incompatible with the bundled posthog library, producing noisy stderr warnings +# on every client operation ("Failed to send telemetry event … capture() takes +# 1 positional argument but 3 were given"). Silence just that logger. +logging.getLogger("chromadb.telemetry.product.posthog").setLevel(logging.CRITICAL) + +# ONNX Runtime's CoreML provider segfaults during vector queries on Apple Silicon. +# Force CPU execution unless the user has explicitly set a preference. +if platform.machine() == "arm64" and platform.system() == "Darwin": + os.environ.setdefault("ORT_DISABLE_COREML", "1") __all__ = ["main", "__version__"] From f1a8220f297edd76b2190a40cb46682346750c07 Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Wed, 8 Apr 2026 13:02:47 +0200 Subject: [PATCH 04/38] Fixed broken link reference for AAAK Dialect in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d05952f..cc106c8 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Other memory systems try to fix this by letting AI decide what's worth rememberi
-[Quick Start](#quick-start) · [The Palace](#the-palace) · [AAAK Dialect](#aaak-compression) · [Benchmarks](#benchmarks) · [MCP Tools](#mcp-server) +[Quick Start](#quick-start) · [The Palace](#the-palace) · [AAAK Dialect](#aaak-dialect-experimental) · [Benchmarks](#benchmarks) · [MCP Tools](#mcp-server)
From 3d00a936552af50515cd1f7d14bcbbbf7ff4e640 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Wed, 8 Apr 2026 14:55:46 +0300 Subject: [PATCH 05/38] feat: add MemPalace Claude Code plugin with hooks and instructions - Introduced README.md for plugin overview and installation instructions. - Added hooks configuration in hooks.json for auto-save and pre-compact functionality. - Implemented stop and pre-compact hooks in bash scripts for memory management. - Created marketplace.json and plugin.json for plugin metadata and versioning. - Developed skills and instructions for help, init, mine, search, and status functionalities. - Added CLI commands for executing hooks and displaying skill instructions. - Implemented hooks_cli.py for handling hook logic and JSON input/output. - Enhanced instruction files for user guidance on setup and usage. - Updated .gitignore to exclude additional files. - Created GitHub Actions workflow for syncing plugin version on push. --- .claude-plugin/README.md | 60 +++++ .claude-plugin/hooks/hooks.json | 25 +++ .../hooks/mempal-precompact-hook.sh | 5 + .claude-plugin/hooks/mempal-stop-hook.sh | 5 + .claude-plugin/marketplace.json | 18 ++ .claude-plugin/plugin.json | 33 +++ .claude-plugin/skills/help/SKILL.md | 12 + .claude-plugin/skills/init/SKILL.md | 18 ++ .claude-plugin/skills/mine/SKILL.md | 12 + .claude-plugin/skills/search/SKILL.md | 12 + .claude-plugin/skills/status/SKILL.md | 12 + .github/workflows/bump-plugin-version.yml | 34 +++ .gitignore | 1 + mempalace/cli.py | 60 +++++ mempalace/hooks_cli.py | 208 ++++++++++++++++++ mempalace/instructions/help.md | 105 +++++++++ mempalace/instructions/init.md | 69 ++++++ mempalace/instructions/mine.md | 64 ++++++ mempalace/instructions/search.md | 57 +++++ mempalace/instructions/status.md | 49 +++++ mempalace/instructions_cli.py | 28 +++ 21 files changed, 887 insertions(+) create mode 100644 .claude-plugin/README.md create mode 100644 .claude-plugin/hooks/hooks.json create mode 100644 .claude-plugin/hooks/mempal-precompact-hook.sh create mode 100644 .claude-plugin/hooks/mempal-stop-hook.sh create mode 100644 .claude-plugin/marketplace.json create mode 100644 .claude-plugin/plugin.json create mode 100644 .claude-plugin/skills/help/SKILL.md create mode 100644 .claude-plugin/skills/init/SKILL.md create mode 100644 .claude-plugin/skills/mine/SKILL.md create mode 100644 .claude-plugin/skills/search/SKILL.md create mode 100644 .claude-plugin/skills/status/SKILL.md create mode 100644 .github/workflows/bump-plugin-version.yml create mode 100644 mempalace/hooks_cli.py create mode 100644 mempalace/instructions/help.md create mode 100644 mempalace/instructions/init.md create mode 100644 mempalace/instructions/mine.md create mode 100644 mempalace/instructions/search.md create mode 100644 mempalace/instructions/status.md create mode 100644 mempalace/instructions_cli.py diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md new file mode 100644 index 0000000..6754626 --- /dev/null +++ b/.claude-plugin/README.md @@ -0,0 +1,60 @@ +# MemPalace Claude Code Plugin + +A Claude Code plugin that gives your AI a persistent memory system. Mine projects and conversations into a searchable palace backed by ChromaDB, with 19 MCP tools, auto-save hooks, and 5 guided skills. + +## Prerequisites + +- Python 3.9+ + +## Installation + +### Claude Code Marketplace + +```bash +claude plugin add mempalace +``` + +### Git + +```bash +claude plugin add --git https://github.com/milla-jovovich/mempalace +``` + +### Local Clone + +```bash +claude plugin add /path/to/mempalace +``` + +## Post-Install Setup + +After installing the plugin, run the init command to complete setup (pip install, MCP configuration, etc.): + +``` +/mempalace:init +``` + +## Available Slash Commands + +| Command | Description | +|---------|-------------| +| `/mempalace:help` | Show available tools, skills, and architecture | +| `/mempalace:init` | Set up MemPalace -- install, configure MCP, onboard | +| `/mempalace:search` | Search your memories across the palace | +| `/mempalace:mine` | Mine projects and conversations into the palace | +| `/mempalace:status` | Show palace overview -- wings, rooms, drawer counts | + +## Hooks + +MemPalace registers two hooks that run automatically: + +- **Stop** -- Saves conversation context when a session ends. +- **PreCompact** -- Preserves important memories before context compaction. + +## MCP Server + +The plugin automatically configures a local MCP server with 19 tools for storing, searching, and managing memories. No manual MCP setup is required -- `/mempalace:init` handles everything. + +## Full Documentation + +See the main [README](../README.md) for complete documentation, architecture details, and advanced usage. diff --git a/.claude-plugin/hooks/hooks.json b/.claude-plugin/hooks/hooks.json new file mode 100644 index 0000000..f1f0a90 --- /dev/null +++ b/.claude-plugin/hooks/hooks.json @@ -0,0 +1,25 @@ +{ + "description": "MemPalace auto-save and pre-compact hooks", + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/mempal-stop-hook.sh" + } + ] + } + ], + "PreCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/mempal-precompact-hook.sh" + } + ] + } + ] + } +} diff --git a/.claude-plugin/hooks/mempal-precompact-hook.sh b/.claude-plugin/hooks/mempal-precompact-hook.sh new file mode 100644 index 0000000..0ac46dd --- /dev/null +++ b/.claude-plugin/hooks/mempal-precompact-hook.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# MemPalace PreCompact Hook — thin wrapper calling Python CLI +# All logic lives in mempalace.hooks_cli for cross-harness extensibility +INPUT=$(cat) +echo "$INPUT" | python3 -m mempalace hook run --hook precompact --harness claude-code diff --git a/.claude-plugin/hooks/mempal-stop-hook.sh b/.claude-plugin/hooks/mempal-stop-hook.sh new file mode 100644 index 0000000..cba3284 --- /dev/null +++ b/.claude-plugin/hooks/mempal-stop-hook.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# MemPalace Stop Hook — thin wrapper calling Python CLI +# All logic lives in mempalace.hooks_cli for cross-harness extensibility +INPUT=$(cat) +echo "$INPUT" | python3 -m mempalace hook run --hook stop --harness claude-code diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..0bf890c --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,18 @@ +{ + "name": "mempalace", + "owner": { + "name": "milla-jovovich", + "url": "https://github.com/milla-jovovich" + }, + "plugins": [ + { + "name": "mempalace", + "source": ".", + "description": "AI memory system — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, guided setup.", + "version": "3.0.0", + "author": { + "name": "milla-jovovich" + } + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..df7f544 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "mempalace", + "version": "3.0.0", + "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", + "author": { "name": "milla-jovovich" }, + "license": "MIT", + "hooks": { + "Stop": "hooks/mempal-stop-hook.sh", + "PreCompact": "hooks/mempal-precompact-hook.sh" + }, + "skills": [ + { "name": "init", "file": "skills/init/SKILL.md" }, + { "name": "search", "file": "skills/search/SKILL.md" }, + { "name": "mine", "file": "skills/mine/SKILL.md" }, + { "name": "help", "file": "skills/help/SKILL.md" }, + { "name": "status", "file": "skills/status/SKILL.md" } + ], + "commands": [ + { "name": "mempalace:help", "description": "Show MemPalace help — available tools, skills, architecture", "skill": "help" }, + { "name": "mempalace:init", "description": "Set up MemPalace — install, configure MCP, onboard", "skill": "init" }, + { "name": "mempalace:search", "description": "Search your memories across the palace", "skill": "search" }, + { "name": "mempalace:mine", "description": "Mine projects and conversations into the palace", "skill": "mine" }, + { "name": "mempalace:status", "description": "Show palace overview — wings, rooms, drawer counts", "skill": "status" } + ], + "mcp": { + "mempalace": { + "command": "python3", + "args": ["-m", "mempalace.mcp_server"] + } + }, + "keywords": ["memory", "ai", "rag", "mcp", "chromadb", "palace", "search"], + "repository": { "type": "git", "url": "https://github.com/milla-jovovich/mempalace" } +} diff --git a/.claude-plugin/skills/help/SKILL.md b/.claude-plugin/skills/help/SKILL.md new file mode 100644 index 0000000..d389a1b --- /dev/null +++ b/.claude-plugin/skills/help/SKILL.md @@ -0,0 +1,12 @@ +--- +name: help +description: Show comprehensive MemPalace help — available skills, MCP tools, CLI commands, hooks, and architecture. +--- + +Run the following command and display its output to the user: + +```bash +mempalace instructions help +``` + +Display the output as-is — it's pre-formatted markdown. diff --git a/.claude-plugin/skills/init/SKILL.md b/.claude-plugin/skills/init/SKILL.md new file mode 100644 index 0000000..a255711 --- /dev/null +++ b/.claude-plugin/skills/init/SKILL.md @@ -0,0 +1,18 @@ +--- +name: init +description: Set up MemPalace — install the package, initialize a palace, configure MCP server, and verify everything works. +--- + +Run the following command to get setup instructions, then follow them step by step: + +```bash +mempalace instructions init +``` + +If the command fails (mempalace not installed yet), first install it: + +```bash +pip install mempalace +``` + +Then run the instructions command again and follow the output. diff --git a/.claude-plugin/skills/mine/SKILL.md b/.claude-plugin/skills/mine/SKILL.md new file mode 100644 index 0000000..8896ed7 --- /dev/null +++ b/.claude-plugin/skills/mine/SKILL.md @@ -0,0 +1,12 @@ +--- +name: mine +description: Mine projects and conversations into the MemPalace. Supports project files, conversation exports, and auto-classification. +--- + +Run the following command to get mining instructions, then follow them: + +```bash +mempalace instructions mine +``` + +Follow the returned instructions to mine the user's data. diff --git a/.claude-plugin/skills/search/SKILL.md b/.claude-plugin/skills/search/SKILL.md new file mode 100644 index 0000000..6ad2d24 --- /dev/null +++ b/.claude-plugin/skills/search/SKILL.md @@ -0,0 +1,12 @@ +--- +name: search +description: Search your memories across the MemPalace using semantic search with wing/room filtering. +--- + +Run the following command to get search instructions, then follow them: + +```bash +mempalace instructions search +``` + +Follow the returned instructions to execute the user's search query. diff --git a/.claude-plugin/skills/status/SKILL.md b/.claude-plugin/skills/status/SKILL.md new file mode 100644 index 0000000..dca3646 --- /dev/null +++ b/.claude-plugin/skills/status/SKILL.md @@ -0,0 +1,12 @@ +--- +name: status +description: Show the current state of your memory palace — wings, rooms, drawer counts, and suggestions. +--- + +Run the following command to get status instructions, then follow them: + +```bash +mempalace instructions status +``` + +Follow the returned instructions to display the palace status. diff --git a/.github/workflows/bump-plugin-version.yml b/.github/workflows/bump-plugin-version.yml new file mode 100644 index 0000000..8735f51 --- /dev/null +++ b/.github/workflows/bump-plugin-version.yml @@ -0,0 +1,34 @@ +name: Sync Plugin Version + +on: + push: + branches: [main] + +jobs: + sync-version: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + + - name: Extract version from version.py + id: version + run: | + VERSION=$(python3 -c "exec(open('mempalace/version.py').read()); print(__version__)") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Update plugin.json + run: | + jq --arg v "${{ steps.version.outputs.version }}" '.version = $v' .claude-plugin/plugin.json > tmp.json && mv tmp.json .claude-plugin/plugin.json + + - name: Update marketplace.json + run: | + jq --arg v "${{ steps.version.outputs.version }}" '.plugins[0].version = $v' .claude-plugin/marketplace.json > tmp.json && mv tmp.json .claude-plugin/marketplace.json + + - name: Commit if changed + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add .claude-plugin/plugin.json .claude-plugin/marketplace.json + git diff --staged --quiet || git commit -m "chore: sync plugin version to ${{ steps.version.outputs.version }}" && git push diff --git a/.gitignore b/.gitignore index 54c2d1b..c8b10cc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ *.pyc .pytest_cache/ mempal.yaml +.a5c/ diff --git a/mempalace/cli.py b/mempalace/cli.py index 1599b08..9d2465b 100644 --- a/mempalace/cli.py +++ b/mempalace/cli.py @@ -226,6 +226,20 @@ def cmd_repair(args): print(f"\n{'=' * 55}\n") +def cmd_hook(args): + """Run hook logic: reads JSON from stdin, outputs JSON to stdout.""" + from .hooks_cli import run_hook + + run_hook(hook_name=args.hook, harness=args.harness) + + +def cmd_instructions(args): + """Output skill instructions to stdout.""" + from .instructions_cli import run_instructions + + run_instructions(name=args.name) + + def cmd_compress(args): """Compress drawers in a wing using AAAK Dialect.""" import chromadb @@ -451,6 +465,35 @@ def main(): help="Only split files containing at least N sessions (default: 2)", ) + # hook + p_hook = sub.add_parser( + "hook", + help="Run hook logic (reads JSON from stdin, outputs JSON to stdout)", + ) + hook_sub = p_hook.add_subparsers(dest="hook_action") + p_hook_run = hook_sub.add_parser("run", help="Execute a hook") + p_hook_run.add_argument( + "--hook", + required=True, + choices=["stop", "precompact"], + help="Hook name to run", + ) + p_hook_run.add_argument( + "--harness", + required=True, + choices=["claude-code"], + help="Harness type (determines stdin JSON format)", + ) + + # instructions + p_instructions = sub.add_parser( + "instructions", + help="Output skill instructions to stdout", + ) + instructions_sub = p_instructions.add_subparsers(dest="instructions_name") + for instr_name in ["init", "search", "mine", "help", "status"]: + instructions_sub.add_parser(instr_name, help=f"Output {instr_name} instructions") + # repair sub.add_parser( "repair", @@ -466,6 +509,23 @@ def main(): parser.print_help() return + # Handle two-level subcommands + if args.command == "hook": + if not getattr(args, "hook_action", None): + p_hook.print_help() + return + cmd_hook(args) + return + + if args.command == "instructions": + name = getattr(args, "instructions_name", None) + if not name: + p_instructions.print_help() + return + args.name = name + cmd_instructions(args) + return + dispatch = { "init": cmd_init, "mine": cmd_mine, diff --git a/mempalace/hooks_cli.py b/mempalace/hooks_cli.py new file mode 100644 index 0000000..6e0ec0a --- /dev/null +++ b/mempalace/hooks_cli.py @@ -0,0 +1,208 @@ +""" +Hook logic for MemPalace — Python implementation of stop and precompact hooks. + +Reads JSON from stdin, outputs JSON to stdout. +Supported hooks: stop, precompact +Supported harnesses: claude-code (extensible to cursor, gemini, etc.) +""" + +import json +import os +import re +import subprocess +import sys +from datetime import datetime +from pathlib import Path + +SAVE_INTERVAL = 15 +STATE_DIR = Path.home() / ".mempalace" / "hook_state" + +STOP_BLOCK_REASON = ( + "AUTO-SAVE checkpoint. Save key topics, decisions, quotes, and code " + "from this session to your memory system. Organize into appropriate " + "categories. Use verbatim quotes where possible. Continue conversation " + "after saving." +) + +PRECOMPACT_BLOCK_REASON = ( + "COMPACTION IMMINENT. Save ALL topics, decisions, quotes, code, and " + "important context from this session to your memory system. Be thorough " + "\u2014 after compaction, detailed context will be lost. Organize into " + "appropriate categories. Use verbatim quotes where possible. Save " + "everything, then allow compaction to proceed." +) + + +def _sanitize_session_id(session_id: str) -> str: + """Only allow alnum, dash, underscore to prevent path traversal.""" + sanitized = re.sub(r"[^a-zA-Z0-9_-]", "", session_id) + return sanitized or "unknown" + + +def _count_human_messages(transcript_path: str) -> int: + """Count human messages in a JSONL transcript, skipping command-messages.""" + path = Path(transcript_path).expanduser() + if not path.is_file(): + return 0 + count = 0 + try: + with open(path) as f: + for line in f: + try: + entry = json.loads(line) + msg = entry.get("message", {}) + if isinstance(msg, dict) and msg.get("role") == "user": + content = msg.get("content", "") + if isinstance(content, str) and "" in content: + continue + count += 1 + except (json.JSONDecodeError, AttributeError): + pass + except OSError: + return 0 + return count + + +def _log(message: str): + """Append to hook state log file.""" + try: + STATE_DIR.mkdir(parents=True, exist_ok=True) + log_path = STATE_DIR / "hook.log" + timestamp = datetime.now().strftime("%H:%M:%S") + with open(log_path, "a") as f: + f.write(f"[{timestamp}] {message}\n") + except OSError: + pass + + +def _output(data: dict): + """Print JSON to stdout with consistent formatting (pretty-printed).""" + print(json.dumps(data, indent=2, ensure_ascii=False)) + + +def _maybe_auto_ingest(): + """If MEMPAL_DIR is set and exists, run mempalace mine in background.""" + mempal_dir = os.environ.get("MEMPAL_DIR", "") + if mempal_dir and os.path.isdir(mempal_dir): + try: + log_path = STATE_DIR / "hook.log" + with open(log_path, "a") as log_f: + subprocess.Popen( + [sys.executable, "-m", "mempalace", "mine", mempal_dir], + stdout=log_f, + stderr=log_f, + ) + except OSError: + pass + + +def _parse_claude_code_input(data: dict) -> dict: + """Parse stdin JSON for the claude-code harness.""" + return { + "session_id": _sanitize_session_id(str(data.get("session_id", "unknown"))), + "stop_hook_active": data.get("stop_hook_active", False), + "transcript_path": str(data.get("transcript_path", "")), + } + + +def _parse_harness_input(data: dict, harness: str) -> dict: + """Parse stdin JSON according to the harness type.""" + parsers = { + "claude-code": _parse_claude_code_input, + } + parser = parsers.get(harness) + if parser is None: + print(f"Unknown harness: {harness}", file=sys.stderr) + sys.exit(1) + return parser(data) + + +def hook_stop(data: dict, harness: str): + """Stop hook: block every N messages for auto-save.""" + parsed = _parse_harness_input(data, harness) + session_id = parsed["session_id"] + stop_hook_active = parsed["stop_hook_active"] + transcript_path = parsed["transcript_path"] + + # If already in a save cycle, let through (infinite-loop prevention) + if stop_hook_active in (True, "True", "true"): + _output({}) + return + + # Count human messages + exchange_count = _count_human_messages(transcript_path) + + # Track last save point + STATE_DIR.mkdir(parents=True, exist_ok=True) + last_save_file = STATE_DIR / f"{session_id}_last_save" + last_save = 0 + if last_save_file.is_file(): + try: + last_save = int(last_save_file.read_text().strip()) + except (ValueError, OSError): + last_save = 0 + + since_last = exchange_count - last_save + + _log(f"Session {session_id}: {exchange_count} exchanges, {since_last} since last save") + + if since_last >= SAVE_INTERVAL and exchange_count > 0: + # Update last save point + try: + last_save_file.write_text(str(exchange_count)) + except OSError: + pass + + _log(f"TRIGGERING SAVE at exchange {exchange_count}") + + # Optional: auto-ingest if MEMPAL_DIR is set + _maybe_auto_ingest() + + _output({"decision": "block", "reason": STOP_BLOCK_REASON}) + else: + _output({}) + + +def hook_precompact(data: dict, harness: str): + """Precompact hook: always block with comprehensive save instruction.""" + parsed = _parse_harness_input(data, harness) + session_id = parsed["session_id"] + + _log(f"PRE-COMPACT triggered for session {session_id}") + + # Optional: auto-ingest synchronously before compaction (so memories land first) + mempal_dir = os.environ.get("MEMPAL_DIR", "") + if mempal_dir and os.path.isdir(mempal_dir): + try: + log_path = STATE_DIR / "hook.log" + with open(log_path, "a") as log_f: + subprocess.run( + [sys.executable, "-m", "mempalace", "mine", mempal_dir], + stdout=log_f, + stderr=log_f, + ) + except OSError: + pass + + # Always block -- compaction = save everything + _output({"decision": "block", "reason": PRECOMPACT_BLOCK_REASON}) + + +def run_hook(hook_name: str, harness: str): + """Main entry point: read stdin JSON, dispatch to hook handler.""" + try: + data = json.load(sys.stdin) + except (json.JSONDecodeError, EOFError): + data = {} + + hooks = { + "stop": hook_stop, + "precompact": hook_precompact, + } + + handler = hooks.get(hook_name) + if handler is None: + print(f"Unknown hook: {hook_name}", file=sys.stderr) + sys.exit(1) + + handler(data, harness) diff --git a/mempalace/instructions/help.md b/mempalace/instructions/help.md new file mode 100644 index 0000000..f18c1de --- /dev/null +++ b/mempalace/instructions/help.md @@ -0,0 +1,105 @@ +# MemPalace + +AI memory system. Store everything, find anything. Local, free, no API key. + +--- + +## Slash Commands + +| Command | Description | +|----------------------|--------------------------------| +| /mempalace:init | Install and set up MemPalace | +| /mempalace:search | Search your memories | +| /mempalace:mine | Mine projects and conversations| +| /mempalace:status | Palace overview and stats | +| /mempalace:help | This help message | + +--- + +## MCP Tools (19) + +### Palace (read) +- mempalace_status -- Palace status and stats +- mempalace_list_wings -- List all wings +- mempalace_list_rooms -- List rooms in a wing +- mempalace_get_taxonomy -- Get the full taxonomy tree +- mempalace_search -- Search memories by query +- mempalace_check_duplicate -- Check if a memory already exists +- mempalace_get_aaak_spec -- Get the AAAK specification + +### Palace (write) +- mempalace_add_drawer -- Add a new memory (drawer) +- mempalace_delete_drawer -- Delete a memory (drawer) + +### Knowledge Graph +- mempalace_kg_query -- Query the knowledge graph +- mempalace_kg_add -- Add a knowledge graph entry +- mempalace_kg_invalidate -- Invalidate a knowledge graph entry +- mempalace_kg_timeline -- View knowledge graph timeline +- mempalace_kg_stats -- Knowledge graph statistics + +### Navigation +- mempalace_traverse -- Traverse the palace structure +- mempalace_find_tunnels -- Find cross-wing connections +- mempalace_graph_stats -- Graph connectivity statistics + +### Agent Diary +- mempalace_diary_write -- Write a diary entry +- mempalace_diary_read -- Read diary entries + +--- + +## CLI Commands + + mempalace init Initialize a new palace + mempalace mine Mine a project (default mode) + mempalace mine --mode convos Mine conversation exports + mempalace search "query" Search your memories + mempalace split Split large transcript files + mempalace wake-up Load palace into context + mempalace compress Compress palace storage + mempalace status Show palace status + mempalace repair Rebuild vector index + mempalace hook run Run hook logic (for harness integration) + mempalace instructions Output skill instructions + +--- + +## Auto-Save Hooks + +- Stop hook -- Automatically saves memories every 15 messages. Counts human + messages in the session transcript (skipping command-messages). When the + threshold is reached, blocks the AI with a save instruction. Uses + ~/.mempalace/hook_state/ to track save points per session. If + stop_hook_active is true, passes through to prevent infinite loops. + +- PreCompact hook -- Emergency save before context compaction. Always blocks + with a comprehensive save instruction because compaction means the AI is + about to lose detailed context. + +Hooks read JSON from stdin and output JSON to stdout. They can be invoked via: + + echo '{"session_id":"abc","stop_hook_active":false,"transcript_path":"..."}' | mempalace hook run --hook stop --harness claude-code + +--- + +## Architecture + + Wings (projects/people) + +-- Rooms (topics) + +-- Closets (summaries) + +-- Drawers (verbatim memories) + + Halls connect rooms within a wing. + Tunnels connect rooms across wings. + +The palace is stored locally using ChromaDB for vector search and SQLite for +metadata. No cloud services or API keys required. + +--- + +## Getting Started + +1. /mempalace:init -- Set up your palace +2. /mempalace:mine -- Mine a project or conversation +3. /mempalace:search -- Find what you stored diff --git a/mempalace/instructions/init.md b/mempalace/instructions/init.md new file mode 100644 index 0000000..40fe8fc --- /dev/null +++ b/mempalace/instructions/init.md @@ -0,0 +1,69 @@ +# MemPalace Init + +Guide the user through a complete MemPalace setup. Follow each step in order, +stopping to report errors and attempt remediation before proceeding. + +## Step 1: Check Python version + +Run `python3 --version` (or `python --version` on Windows) and confirm the +version is 3.9 or higher. If Python is not found or the version is too old, +tell the user they need Python 3.9+ installed and stop. + +## Step 2: Check if mempalace is already installed + +Run `pip show mempalace` to see if the package is already present. If it is, +report the installed version and skip to Step 4. + +## Step 3: Install mempalace + +Run `pip install mempalace`. + +### Error handling -- pip failures + +If `pip install mempalace` fails, try these fallbacks in order: + +1. Try `pip3 install mempalace` +2. Try `python -m pip install mempalace` (or `python3 -m pip install mempalace`) +3. If the error mentions missing build tools or compilation failures (commonly + from chromadb or its native dependencies): + - On Linux/macOS: suggest `sudo apt-get install build-essential python3-dev` + (Debian/Ubuntu) or `xcode-select --install` (macOS) + - On Windows: suggest installing Microsoft C++ Build Tools from + https://visualstudio.microsoft.com/visual-cpp-build-tools/ + - Then retry the install command +4. If all attempts fail, report the error clearly and stop. + +## Step 4: Ask for project directory + +Ask the user which project directory they want to initialize with MemPalace. +Offer the current working directory as the default. Wait for their response +before continuing. + +## Step 5: Initialize the palace + +Run `mempalace init ` where `` is the directory from Step 4. + +If this fails, report the error and stop. + +## Step 6: Configure MCP server + +Run the following command to register the MemPalace MCP server with Claude: + + claude mcp add mempalace -- python -m mempalace.mcp_server + +If this fails, report the error but continue to the next step (MCP +configuration can be done manually later). + +## Step 7: Verify installation + +Run `mempalace status` and confirm the output shows a healthy palace. + +If the command fails or reports errors, walk the user through troubleshooting +based on the output. + +## Step 8: Show next steps + +Tell the user setup is complete and suggest these next actions: + +- Use /mempalace:mine to start adding data to their palace +- Use /mempalace:search to query their palace and retrieve stored knowledge diff --git a/mempalace/instructions/mine.md b/mempalace/instructions/mine.md new file mode 100644 index 0000000..ec8c250 --- /dev/null +++ b/mempalace/instructions/mine.md @@ -0,0 +1,64 @@ +# MemPalace Mine + +When the user invokes this skill, follow these steps: + +## 1. Ask what to mine + +Ask the user what they want to mine and where the source data is located. +Clarify: +- Is it a project directory (code, docs, notes)? +- Is it conversation exports (Claude, ChatGPT, Slack)? +- Do they want auto-classification (decisions, milestones, problems)? + +## 2. Choose the mining mode + +There are three mining modes: + +### Project mining + + mempalace mine + +Mines code files, documentation, and notes from a project directory. + +### Conversation mining + + mempalace mine --mode convos + +Mines conversation exports from Claude, ChatGPT, or Slack into the palace. + +### General extraction (auto-classify) + + mempalace mine --mode convos --extract general + +Auto-classifies mined content into decisions, milestones, and problems. + +## 3. Optionally split mega-files first + +If the source directory contains very large files, suggest splitting them +before mining: + + mempalace split [--dry-run] + +Use --dry-run first to preview what will be split without making changes. + +## 4. Optionally tag with a wing + +If the user wants to organize mined content under a specific wing, add the +--wing flag: + + mempalace mine --wing + +## 5. Show progress and results + +Run the selected mining command and display progress as it executes. After +completion, summarize the results including: +- Number of items mined +- Categories or classifications applied +- Any warnings or skipped files + +## 6. Suggest next steps + +After mining completes, suggest the user try: +- /mempalace:search -- search the newly mined content +- /mempalace:status -- check the current state of their palace +- Mine more data from additional sources diff --git a/mempalace/instructions/search.md b/mempalace/instructions/search.md new file mode 100644 index 0000000..0b6a813 --- /dev/null +++ b/mempalace/instructions/search.md @@ -0,0 +1,57 @@ +# MemPalace Search + +When the user wants to search their MemPalace memories, follow these steps: + +## 1. Parse the Search Query + +Extract the core search intent from the user's message. Identify any explicit +or implicit filters: +- Wing -- a top-level category (e.g., "work", "personal", "research") +- Room -- a sub-category within a wing +- Keywords / semantic query -- the actual search terms + +## 2. Determine Wing/Room Filters + +If the user mentions a specific domain, topic area, or context, map it to the +appropriate wing and/or room. If unsure, omit filters to search globally. You +can discover the taxonomy first if needed. + +## 3. Use MCP Tools (Preferred) + +If MCP tools are available, use them in this priority order: + +- mempalace_search(query, wing, room) -- Primary search tool. Pass the semantic + query and any wing/room filters. +- mempalace_list_wings -- Discover all available wings. Use when the user asks + what categories exist or you need to resolve a wing name. +- mempalace_list_rooms(wing) -- List rooms within a specific wing. Use to help + the user navigate or to resolve a room name. +- mempalace_get_taxonomy -- Retrieve the full wing/room/drawer tree. Use when + the user wants an overview of their entire memory structure. +- mempalace_traverse(room) -- Walk the knowledge graph starting from a room. + Use when the user wants to explore connections and related memories. +- mempalace_find_tunnels(wing1, wing2) -- Find cross-wing connections (tunnels) + between two wings. Use when the user asks about relationships between + different knowledge domains. + +## 4. CLI Fallback + +If MCP tools are not available, fall back to the CLI: + + mempalace search "query" [--wing X] [--room Y] + +## 5. Present Results + +When presenting search results: +- Always include source attribution: wing, room, and drawer for each result +- Show relevance or similarity scores if available +- Group results by wing/room when returning multiple hits +- Quote or summarize the memory content clearly + +## 6. Offer Next Steps + +After presenting results, offer the user options to go deeper: +- Drill deeper -- search within a specific room or narrow the query +- Traverse -- explore the knowledge graph from a related room +- Check tunnels -- look for cross-wing connections if the topic spans domains +- Browse taxonomy -- show the full structure for manual exploration diff --git a/mempalace/instructions/status.md b/mempalace/instructions/status.md new file mode 100644 index 0000000..ceb902b --- /dev/null +++ b/mempalace/instructions/status.md @@ -0,0 +1,49 @@ +# MemPalace Status + +Display the current state of the user's memory palace. + +## Step 1: Gather Palace Status + +Check if MCP tools are available (look for mempalace_status in available tools). + +- If MCP is available: Call the mempalace_status tool to retrieve palace state. +- If MCP is not available: Run the CLI command: mempalace status + +## Step 2: Display Wing/Room/Drawer Counts + +Present the palace structure counts clearly: +- Number of wings +- Number of rooms +- Number of drawers +- Total memories stored + +Keep the output concise -- use a brief summary format, not verbose tables. + +## Step 3: Knowledge Graph Stats (MCP only) + +If MCP tools are available, also call: +- mempalace_kg_stats -- for a knowledge graph overview (triple count, entity + count, relationship types) +- mempalace_graph_stats -- for connectivity information (connected components, + average connections per entity) + +Present these alongside the palace counts in a unified summary. + +## Step 4: Suggest Next Actions + +Based on the current state, suggest one relevant action: + +- Empty palace (zero memories): Suggest "Try /mempalace:mine to add data from + files, URLs, or text." +- Has data but no knowledge graph (memories exist but KG stats show zero + triples): Suggest "Consider adding knowledge graph triples for richer + queries." +- Healthy palace (has memories and KG data): Suggest "Use /mempalace:search to + query your memories." + +## Output Style + +- Be concise and informative -- aim for a quick glance, not a report. +- Use short labels and numbers, not prose paragraphs. +- If any step fails or a tool is unavailable, note it briefly and continue + with what is available. diff --git a/mempalace/instructions_cli.py b/mempalace/instructions_cli.py new file mode 100644 index 0000000..239d721 --- /dev/null +++ b/mempalace/instructions_cli.py @@ -0,0 +1,28 @@ +""" +Instruction text output for MemPalace CLI commands. + +Each instruction lives as a .md file in the instructions/ directory +inside the package. The CLI reads and prints the file content. +""" + +import sys +from pathlib import Path + +INSTRUCTIONS_DIR = Path(__file__).parent / "instructions" + +AVAILABLE = ["init", "search", "mine", "help", "status"] + + +def run_instructions(name: str): + """Read and print the instruction .md file for the given name.""" + if name not in AVAILABLE: + print(f"Unknown instructions: {name}", file=sys.stderr) + print(f"Available: {', '.join(sorted(AVAILABLE))}", file=sys.stderr) + sys.exit(1) + + md_path = INSTRUCTIONS_DIR / f"{name}.md" + if not md_path.is_file(): + print(f"Instructions file not found: {md_path}", file=sys.stderr) + sys.exit(1) + + print(md_path.read_text()) From 6b5e869dd517272a177d84dee1984de39d0222d9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 11:56:13 +0000 Subject: [PATCH 06/38] chore: sync plugin version to 3.0.0 --- .claude-plugin/plugin.json | 79 +++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index df7f544..f438433 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -2,32 +2,83 @@ "name": "mempalace", "version": "3.0.0", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", - "author": { "name": "milla-jovovich" }, + "author": { + "name": "milla-jovovich" + }, "license": "MIT", "hooks": { "Stop": "hooks/mempal-stop-hook.sh", "PreCompact": "hooks/mempal-precompact-hook.sh" }, "skills": [ - { "name": "init", "file": "skills/init/SKILL.md" }, - { "name": "search", "file": "skills/search/SKILL.md" }, - { "name": "mine", "file": "skills/mine/SKILL.md" }, - { "name": "help", "file": "skills/help/SKILL.md" }, - { "name": "status", "file": "skills/status/SKILL.md" } + { + "name": "init", + "file": "skills/init/SKILL.md" + }, + { + "name": "search", + "file": "skills/search/SKILL.md" + }, + { + "name": "mine", + "file": "skills/mine/SKILL.md" + }, + { + "name": "help", + "file": "skills/help/SKILL.md" + }, + { + "name": "status", + "file": "skills/status/SKILL.md" + } ], "commands": [ - { "name": "mempalace:help", "description": "Show MemPalace help — available tools, skills, architecture", "skill": "help" }, - { "name": "mempalace:init", "description": "Set up MemPalace — install, configure MCP, onboard", "skill": "init" }, - { "name": "mempalace:search", "description": "Search your memories across the palace", "skill": "search" }, - { "name": "mempalace:mine", "description": "Mine projects and conversations into the palace", "skill": "mine" }, - { "name": "mempalace:status", "description": "Show palace overview — wings, rooms, drawer counts", "skill": "status" } + { + "name": "mempalace:help", + "description": "Show MemPalace help — available tools, skills, architecture", + "skill": "help" + }, + { + "name": "mempalace:init", + "description": "Set up MemPalace — install, configure MCP, onboard", + "skill": "init" + }, + { + "name": "mempalace:search", + "description": "Search your memories across the palace", + "skill": "search" + }, + { + "name": "mempalace:mine", + "description": "Mine projects and conversations into the palace", + "skill": "mine" + }, + { + "name": "mempalace:status", + "description": "Show palace overview — wings, rooms, drawer counts", + "skill": "status" + } ], "mcp": { "mempalace": { "command": "python3", - "args": ["-m", "mempalace.mcp_server"] + "args": [ + "-m", + "mempalace.mcp_server" + ] } }, - "keywords": ["memory", "ai", "rag", "mcp", "chromadb", "palace", "search"], - "repository": { "type": "git", "url": "https://github.com/milla-jovovich/mempalace" } + "keywords": [ + "memory", + "ai", + "rag", + "mcp", + "chromadb", + "palace", + "search" + ], + "repository": { + "type": "git", + "url": "https://github.com/milla-jovovich/mempalace" + } } From 94a41913a3facd973631027e9c2ea90aaf5ca0a5 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Wed, 8 Apr 2026 15:37:57 +0300 Subject: [PATCH 07/38] fix: update plugin source path in marketplace.json --- .claude-plugin/marketplace.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 0bf890c..c7cd6df 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,7 +7,7 @@ "plugins": [ { "name": "mempalace", - "source": ".", + "source": "./.claude-plugin", "description": "AI memory system — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, guided setup.", "version": "3.0.0", "author": { From 2f83518415eda1591ba6e7930fb17efe34dead6f Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Wed, 8 Apr 2026 18:37:46 +0300 Subject: [PATCH 08/38] refactor: simplify plugin.json by removing unused hooks and commands --- .claude-plugin/plugin.json | 59 ++------------------------------------ 1 file changed, 2 insertions(+), 57 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index f438433..6918b43 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -6,59 +6,7 @@ "name": "milla-jovovich" }, "license": "MIT", - "hooks": { - "Stop": "hooks/mempal-stop-hook.sh", - "PreCompact": "hooks/mempal-precompact-hook.sh" - }, - "skills": [ - { - "name": "init", - "file": "skills/init/SKILL.md" - }, - { - "name": "search", - "file": "skills/search/SKILL.md" - }, - { - "name": "mine", - "file": "skills/mine/SKILL.md" - }, - { - "name": "help", - "file": "skills/help/SKILL.md" - }, - { - "name": "status", - "file": "skills/status/SKILL.md" - } - ], - "commands": [ - { - "name": "mempalace:help", - "description": "Show MemPalace help — available tools, skills, architecture", - "skill": "help" - }, - { - "name": "mempalace:init", - "description": "Set up MemPalace — install, configure MCP, onboard", - "skill": "init" - }, - { - "name": "mempalace:search", - "description": "Search your memories across the palace", - "skill": "search" - }, - { - "name": "mempalace:mine", - "description": "Mine projects and conversations into the palace", - "skill": "mine" - }, - { - "name": "mempalace:status", - "description": "Show palace overview — wings, rooms, drawer counts", - "skill": "status" - } - ], + "skills": "./skills/", "mcp": { "mempalace": { "command": "python3", @@ -77,8 +25,5 @@ "palace", "search" ], - "repository": { - "type": "git", - "url": "https://github.com/milla-jovovich/mempalace" - } + "repository": "https://github.com/milla-jovovich/mempalace" } From c9d395585906058fb27f8bc70bdda9421456e305 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Wed, 8 Apr 2026 18:41:22 +0300 Subject: [PATCH 09/38] refactor: update skill names to include 'mempalace:' prefix for consistency --- .claude-plugin/skills/help/SKILL.md | 2 +- .claude-plugin/skills/init/SKILL.md | 2 +- .claude-plugin/skills/mine/SKILL.md | 2 +- .claude-plugin/skills/search/SKILL.md | 2 +- .claude-plugin/skills/status/SKILL.md | 2 +- .github/workflows/bump-plugin-version.yml | 5 ++++- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/skills/help/SKILL.md b/.claude-plugin/skills/help/SKILL.md index d389a1b..53c0acc 100644 --- a/.claude-plugin/skills/help/SKILL.md +++ b/.claude-plugin/skills/help/SKILL.md @@ -1,5 +1,5 @@ --- -name: help +name: mempalace:help description: Show comprehensive MemPalace help — available skills, MCP tools, CLI commands, hooks, and architecture. --- diff --git a/.claude-plugin/skills/init/SKILL.md b/.claude-plugin/skills/init/SKILL.md index a255711..b6a1c99 100644 --- a/.claude-plugin/skills/init/SKILL.md +++ b/.claude-plugin/skills/init/SKILL.md @@ -1,5 +1,5 @@ --- -name: init +name: mempalace:init description: Set up MemPalace — install the package, initialize a palace, configure MCP server, and verify everything works. --- diff --git a/.claude-plugin/skills/mine/SKILL.md b/.claude-plugin/skills/mine/SKILL.md index 8896ed7..333084a 100644 --- a/.claude-plugin/skills/mine/SKILL.md +++ b/.claude-plugin/skills/mine/SKILL.md @@ -1,5 +1,5 @@ --- -name: mine +name: mempalace:mine description: Mine projects and conversations into the MemPalace. Supports project files, conversation exports, and auto-classification. --- diff --git a/.claude-plugin/skills/search/SKILL.md b/.claude-plugin/skills/search/SKILL.md index 6ad2d24..5884e98 100644 --- a/.claude-plugin/skills/search/SKILL.md +++ b/.claude-plugin/skills/search/SKILL.md @@ -1,5 +1,5 @@ --- -name: search +name: mempalace:search description: Search your memories across the MemPalace using semantic search with wing/room filtering. --- diff --git a/.claude-plugin/skills/status/SKILL.md b/.claude-plugin/skills/status/SKILL.md index dca3646..a244753 100644 --- a/.claude-plugin/skills/status/SKILL.md +++ b/.claude-plugin/skills/status/SKILL.md @@ -1,5 +1,5 @@ --- -name: status +name: mempalace:status description: Show the current state of your memory palace — wings, rooms, drawer counts, and suggestions. --- diff --git a/.github/workflows/bump-plugin-version.yml b/.github/workflows/bump-plugin-version.yml index 8735f51..fc78d76 100644 --- a/.github/workflows/bump-plugin-version.yml +++ b/.github/workflows/bump-plugin-version.yml @@ -31,4 +31,7 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add .claude-plugin/plugin.json .claude-plugin/marketplace.json - git diff --staged --quiet || git commit -m "chore: sync plugin version to ${{ steps.version.outputs.version }}" && git push + if ! git diff --staged --quiet; then + git commit -m "chore: sync plugin version to ${{ steps.version.outputs.version }}" + git push + fi From 94b39cbfe9dd6174ab5cfcd310d1d8d6293ffc9e Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Wed, 8 Apr 2026 18:43:53 +0300 Subject: [PATCH 10/38] refactor: rename workflow and improve version bumping process --- .github/workflows/bump-plugin-version.yml | 28 ++++++++++++++--------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/bump-plugin-version.yml b/.github/workflows/bump-plugin-version.yml index fc78d76..d91c026 100644 --- a/.github/workflows/bump-plugin-version.yml +++ b/.github/workflows/bump-plugin-version.yml @@ -1,37 +1,43 @@ -name: Sync Plugin Version +name: Bump Version on: push: branches: [main] jobs: - sync-version: + bump-version: runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v6 - - name: Extract version from version.py - id: version + - name: Bump patch version run: | - VERSION=$(python3 -c "exec(open('mempalace/version.py').read()); print(__version__)") - echo "version=$VERSION" >> "$GITHUB_OUTPUT" + CURRENT=$(python3 -c "exec(open('mempalace/version.py').read()); print(__version__)") + IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT" + PATCH=$((PATCH + 1)) + NEW="${MAJOR}.${MINOR}.${PATCH}" + echo "__version__ = \"${NEW}\"" > mempalace/version.py + # Prepend docstring + sed -i '1i"""Single source of truth for the MemPalace package version."""\n' mempalace/version.py + echo "version=$NEW" >> "$GITHUB_OUTPUT" + id: version - - name: Update plugin.json + - name: Sync plugin.json run: | jq --arg v "${{ steps.version.outputs.version }}" '.version = $v' .claude-plugin/plugin.json > tmp.json && mv tmp.json .claude-plugin/plugin.json - - name: Update marketplace.json + - name: Sync marketplace.json run: | jq --arg v "${{ steps.version.outputs.version }}" '.plugins[0].version = $v' .claude-plugin/marketplace.json > tmp.json && mv tmp.json .claude-plugin/marketplace.json - - name: Commit if changed + - name: Commit and push run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add .claude-plugin/plugin.json .claude-plugin/marketplace.json + git add mempalace/version.py .claude-plugin/plugin.json .claude-plugin/marketplace.json if ! git diff --staged --quiet; then - git commit -m "chore: sync plugin version to ${{ steps.version.outputs.version }}" + git commit -m "chore: bump version to ${{ steps.version.outputs.version }}" git push fi From a788af8919a555efd0dcbb24b4b64b5a08f474e7 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Wed, 8 Apr 2026 18:49:45 +0300 Subject: [PATCH 11/38] feat: add command documentation for help, init, mine, search, and status --- .claude-plugin/commands/help.md | 6 ++++ .claude-plugin/commands/init.md | 6 ++++ .claude-plugin/commands/mine.md | 7 +++++ .claude-plugin/commands/search.md | 7 +++++ .claude-plugin/commands/status.md | 6 ++++ .claude-plugin/plugin.json | 8 +++++- .claude-plugin/skills/help/SKILL.md | 12 -------- .claude-plugin/skills/init/SKILL.md | 18 ------------ .claude-plugin/skills/mempalace/SKILL.md | 35 ++++++++++++++++++++++++ .claude-plugin/skills/mine/SKILL.md | 12 -------- .claude-plugin/skills/search/SKILL.md | 12 -------- .claude-plugin/skills/status/SKILL.md | 12 -------- 12 files changed, 74 insertions(+), 67 deletions(-) create mode 100644 .claude-plugin/commands/help.md create mode 100644 .claude-plugin/commands/init.md create mode 100644 .claude-plugin/commands/mine.md create mode 100644 .claude-plugin/commands/search.md create mode 100644 .claude-plugin/commands/status.md delete mode 100644 .claude-plugin/skills/help/SKILL.md delete mode 100644 .claude-plugin/skills/init/SKILL.md create mode 100644 .claude-plugin/skills/mempalace/SKILL.md delete mode 100644 .claude-plugin/skills/mine/SKILL.md delete mode 100644 .claude-plugin/skills/search/SKILL.md delete mode 100644 .claude-plugin/skills/status/SKILL.md diff --git a/.claude-plugin/commands/help.md b/.claude-plugin/commands/help.md new file mode 100644 index 0000000..d1c1418 --- /dev/null +++ b/.claude-plugin/commands/help.md @@ -0,0 +1,6 @@ +--- +description: Show comprehensive MemPalace help — available skills, MCP tools, CLI commands, hooks, and architecture. +allowed-tools: Bash, Read +--- + +Invoke the mempalace skill (using the Skill tool) with the `help` command, then follow its instructions. diff --git a/.claude-plugin/commands/init.md b/.claude-plugin/commands/init.md new file mode 100644 index 0000000..a96a64a --- /dev/null +++ b/.claude-plugin/commands/init.md @@ -0,0 +1,6 @@ +--- +description: Set up MemPalace — install the package, initialize a palace, configure MCP server, and verify everything works. +allowed-tools: Bash, Read, Write, Edit, Glob, Grep +--- + +Invoke the mempalace skill (using the Skill tool) with the `init` command, then follow its instructions. diff --git a/.claude-plugin/commands/mine.md b/.claude-plugin/commands/mine.md new file mode 100644 index 0000000..6fa554b --- /dev/null +++ b/.claude-plugin/commands/mine.md @@ -0,0 +1,7 @@ +--- +description: Mine projects and conversations into the MemPalace. Supports project files, conversation exports, and auto-classification. +argument-hint: Path to project or conversation export to mine. +allowed-tools: Bash, Read, Write, Edit, Glob, Grep +--- + +Invoke the mempalace skill (using the Skill tool) with the `mine` command, then follow its instructions. diff --git a/.claude-plugin/commands/search.md b/.claude-plugin/commands/search.md new file mode 100644 index 0000000..092d8bb --- /dev/null +++ b/.claude-plugin/commands/search.md @@ -0,0 +1,7 @@ +--- +description: Search your memories across the MemPalace using semantic search with wing/room filtering. +argument-hint: Search query, optionally with wing/room filters. +allowed-tools: Bash, Read +--- + +Invoke the mempalace skill (using the Skill tool) with the `search` command, then follow its instructions. diff --git a/.claude-plugin/commands/status.md b/.claude-plugin/commands/status.md new file mode 100644 index 0000000..66732e0 --- /dev/null +++ b/.claude-plugin/commands/status.md @@ -0,0 +1,6 @@ +--- +description: Show the current state of your memory palace — wings, rooms, drawer counts, and suggestions. +allowed-tools: Bash, Read +--- + +Invoke the mempalace skill (using the Skill tool) with the `status` command, then follow its instructions. diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 6918b43..974e9bf 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -6,7 +6,13 @@ "name": "milla-jovovich" }, "license": "MIT", - "skills": "./skills/", + "commands": [], + "skills": [ + { + "name": "mempalace", + "file": "skills/mempalace/SKILL.md" + } + ], "mcp": { "mempalace": { "command": "python3", diff --git a/.claude-plugin/skills/help/SKILL.md b/.claude-plugin/skills/help/SKILL.md deleted file mode 100644 index 53c0acc..0000000 --- a/.claude-plugin/skills/help/SKILL.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: mempalace:help -description: Show comprehensive MemPalace help — available skills, MCP tools, CLI commands, hooks, and architecture. ---- - -Run the following command and display its output to the user: - -```bash -mempalace instructions help -``` - -Display the output as-is — it's pre-formatted markdown. diff --git a/.claude-plugin/skills/init/SKILL.md b/.claude-plugin/skills/init/SKILL.md deleted file mode 100644 index b6a1c99..0000000 --- a/.claude-plugin/skills/init/SKILL.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: mempalace:init -description: Set up MemPalace — install the package, initialize a palace, configure MCP server, and verify everything works. ---- - -Run the following command to get setup instructions, then follow them step by step: - -```bash -mempalace instructions init -``` - -If the command fails (mempalace not installed yet), first install it: - -```bash -pip install mempalace -``` - -Then run the instructions command again and follow the output. diff --git a/.claude-plugin/skills/mempalace/SKILL.md b/.claude-plugin/skills/mempalace/SKILL.md new file mode 100644 index 0000000..ae60fca --- /dev/null +++ b/.claude-plugin/skills/mempalace/SKILL.md @@ -0,0 +1,35 @@ +--- +name: mempalace +description: MemPalace — mine projects and conversations into a searchable memory palace. Use when asked about mempalace, memory palace, mining memories, searching memories, or palace setup. +allowed-tools: Bash, Read, Write, Edit, Glob, Grep +--- + +# MemPalace + +A searchable memory palace for AI — mine projects and conversations, then search them semantically. + +## Prerequisites + +Ensure `mempalace` is installed: + +```bash +mempalace --version +``` + +If not installed: + +```bash +pip install mempalace +``` + +## Usage + +MemPalace provides dynamic instructions via the CLI. To get instructions for any operation: + +```bash +mempalace instructions +``` + +Where `` is one of: `help`, `init`, `mine`, `search`, `status`. + +Run the appropriate instructions command, then follow the returned instructions step by step. diff --git a/.claude-plugin/skills/mine/SKILL.md b/.claude-plugin/skills/mine/SKILL.md deleted file mode 100644 index 333084a..0000000 --- a/.claude-plugin/skills/mine/SKILL.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: mempalace:mine -description: Mine projects and conversations into the MemPalace. Supports project files, conversation exports, and auto-classification. ---- - -Run the following command to get mining instructions, then follow them: - -```bash -mempalace instructions mine -``` - -Follow the returned instructions to mine the user's data. diff --git a/.claude-plugin/skills/search/SKILL.md b/.claude-plugin/skills/search/SKILL.md deleted file mode 100644 index 5884e98..0000000 --- a/.claude-plugin/skills/search/SKILL.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: mempalace:search -description: Search your memories across the MemPalace using semantic search with wing/room filtering. ---- - -Run the following command to get search instructions, then follow them: - -```bash -mempalace instructions search -``` - -Follow the returned instructions to execute the user's search query. diff --git a/.claude-plugin/skills/status/SKILL.md b/.claude-plugin/skills/status/SKILL.md deleted file mode 100644 index a244753..0000000 --- a/.claude-plugin/skills/status/SKILL.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: mempalace:status -description: Show the current state of your memory palace — wings, rooms, drawer counts, and suggestions. ---- - -Run the following command to get status instructions, then follow them: - -```bash -mempalace instructions status -``` - -Follow the returned instructions to display the palace status. From d4d328a545beb45ccba7f18d25b5ff51f2dd39a2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 15:50:39 +0000 Subject: [PATCH 12/38] chore: bump version to 3.0.1 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- mempalace/version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c7cd6df..7dfe174 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "mempalace", "source": "./.claude-plugin", "description": "AI memory system — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, guided setup.", - "version": "3.0.0", + "version": "3.0.1", "author": { "name": "milla-jovovich" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 974e9bf..482d97f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.0", + "version": "3.0.1", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/mempalace/version.py b/mempalace/version.py index 08a910f..e5602d1 100644 --- a/mempalace/version.py +++ b/mempalace/version.py @@ -1,3 +1,3 @@ """Single source of truth for the MemPalace package version.""" -__version__ = "3.0.0" +__version__ = "3.0.1" From aaa6f5944670b92e4db2a611666c5b5610703cb4 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Wed, 8 Apr 2026 18:56:00 +0300 Subject: [PATCH 13/38] refactor: remove unused skills section from plugin.json --- .claude-plugin/plugin.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 482d97f..c2e28fc 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -7,12 +7,6 @@ }, "license": "MIT", "commands": [], - "skills": [ - { - "name": "mempalace", - "file": "skills/mempalace/SKILL.md" - } - ], "mcp": { "mempalace": { "command": "python3", From 61924ea018bd9e0973585deae28f19351e8ce9ad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 15:56:25 +0000 Subject: [PATCH 14/38] chore: bump version to 3.0.2 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- mempalace/version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7dfe174..4ee6b5f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "mempalace", "source": "./.claude-plugin", "description": "AI memory system — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, guided setup.", - "version": "3.0.1", + "version": "3.0.2", "author": { "name": "milla-jovovich" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index c2e28fc..482e77a 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.1", + "version": "3.0.2", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/mempalace/version.py b/mempalace/version.py index e5602d1..dba0e3f 100644 --- a/mempalace/version.py +++ b/mempalace/version.py @@ -1,3 +1,3 @@ """Single source of truth for the MemPalace package version.""" -__version__ = "3.0.1" +__version__ = "3.0.2" From 3d68c4127dbf688c247da4487884ed7cd5de279c Mon Sep 17 00:00:00 2001 From: Hsu Hsiuwei Date: Wed, 8 Apr 2026 23:58:45 +0800 Subject: [PATCH 15/38] fix: honour --palace flag in mcp_server Parse --palace before initialising module-level singletons so that both ChromaDB and KnowledgeGraph use the correct palace directory. When --palace is provided the user is requesting an isolated palace; KG must co-locate with ChromaDB under that path, not fall back to the global default (~/.mempalace/knowledge_graph.sqlite3). --- mempalace/mcp_server.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/mempalace/mcp_server.py b/mempalace/mcp_server.py index 2169255..2ff13b3 100644 --- a/mempalace/mcp_server.py +++ b/mempalace/mcp_server.py @@ -2,7 +2,7 @@ """ MemPalace MCP Server — read/write palace access for Claude Code ================================================================ -Install: claude mcp add mempalace -- python -m mempalace.mcp_server +Install: claude mcp add mempalace -- python -m mempalace.mcp_server [--palace /path/to/palace] Tools (read): mempalace_status — total drawers, wing/room breakdown @@ -17,6 +17,8 @@ Tools (write): mempalace_delete_drawer — remove a drawer by ID """ +import argparse +import os import sys import json import logging @@ -31,12 +33,27 @@ import chromadb from .knowledge_graph import KnowledgeGraph -_kg = KnowledgeGraph() - logging.basicConfig(level=logging.INFO, format="%(message)s", stream=sys.stderr) logger = logging.getLogger("mempalace_mcp") + +def _parse_args(): + parser = argparse.ArgumentParser(description="MemPalace MCP Server") + parser.add_argument( + "--palace", + metavar="PATH", + help="Path to the palace directory (overrides config file and env var)", + ) + return parser.parse_args() + + +_args = _parse_args() + +if _args.palace: + os.environ["MEMPALACE_PALACE_PATH"] = os.path.abspath(_args.palace) + _config = MempalaceConfig() +_kg = KnowledgeGraph(db_path=os.path.join(_config.palace_path, "knowledge_graph.sqlite3")) def _get_collection(create=False): From 50c3db383a3fb8827e63f1477b78b922febf1832 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Wed, 8 Apr 2026 19:10:44 +0300 Subject: [PATCH 16/38] feat: add Codex plugin support with hooks, commands, and documentation --- .agents/plugins/marketplace.json | 20 +++++ .claude-plugin/.mcp.json | 9 +++ .claude-plugin/plugin.json | 2 +- .codex-plugin/README.md | 73 +++++++++++++++++++ .codex-plugin/hooks.json | 37 ++++++++++ .codex-plugin/hooks/mempal-precompact-hook.sh | 15 ++++ .../hooks/mempal-session-start-hook.sh | 15 ++++ .codex-plugin/hooks/mempal-stop-hook.sh | 15 ++++ .codex-plugin/plugin.json | 35 +++++++++ .codex-plugin/skills/help/SKILL.md | 13 ++++ .codex-plugin/skills/init/SKILL.md | 13 ++++ .codex-plugin/skills/mine/SKILL.md | 13 ++++ .codex-plugin/skills/search/SKILL.md | 13 ++++ .codex-plugin/skills/status/SKILL.md | 13 ++++ .github/workflows/bump-plugin-version.yml | 6 +- mempalace/cli.py | 4 +- mempalace/hooks_cli.py | 31 +++++++- 17 files changed, 320 insertions(+), 7 deletions(-) create mode 100644 .agents/plugins/marketplace.json create mode 100644 .claude-plugin/.mcp.json create mode 100644 .codex-plugin/README.md create mode 100644 .codex-plugin/hooks.json create mode 100644 .codex-plugin/hooks/mempal-precompact-hook.sh create mode 100644 .codex-plugin/hooks/mempal-session-start-hook.sh create mode 100644 .codex-plugin/hooks/mempal-stop-hook.sh create mode 100644 .codex-plugin/plugin.json create mode 100644 .codex-plugin/skills/help/SKILL.md create mode 100644 .codex-plugin/skills/init/SKILL.md create mode 100644 .codex-plugin/skills/mine/SKILL.md create mode 100644 .codex-plugin/skills/search/SKILL.md create mode 100644 .codex-plugin/skills/status/SKILL.md diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..58223a0 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "mempalace", + "interface": { + "displayName": "MemPalace" + }, + "plugins": [ + { + "name": "mempalace", + "source": { + "source": "local", + "path": "./.codex-plugin" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "NONE" + }, + "category": "Coding" + } + ] +} diff --git a/.claude-plugin/.mcp.json b/.claude-plugin/.mcp.json new file mode 100644 index 0000000..b1e81ed --- /dev/null +++ b/.claude-plugin/.mcp.json @@ -0,0 +1,9 @@ +{ + "mempalace": { + "command": "python3", + "args": [ + "-m", + "mempalace.mcp_server" + ] + } +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 482e77a..488f6f3 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -7,7 +7,7 @@ }, "license": "MIT", "commands": [], - "mcp": { + "mcpServers": { "mempalace": { "command": "python3", "args": [ diff --git a/.codex-plugin/README.md b/.codex-plugin/README.md new file mode 100644 index 0000000..a7ec1de --- /dev/null +++ b/.codex-plugin/README.md @@ -0,0 +1,73 @@ +# MemPalace - Codex CLI Plugin + +Give your AI a persistent memory -- mine projects and conversations into a searchable palace backed by ChromaDB, with 19 MCP tools, auto-save hooks, and guided skills. + +## Prerequisites + +- Python 3.10+ +- Codex CLI installed and configured +- `pip install mempalace` + +## Installation + +### Local Install + +1. Copy or symlink the `.codex-plugin` directory into your project root: + +```bash +cp -r .codex-plugin /path/to/your/project/.codex-plugin +``` + +2. Verify the plugin is detected: + +```bash +codex --plugins +``` + +3. Initialize your palace: + +```bash +codex /init +``` + +### Git Install + +1. Clone the MemPalace repository: + +```bash +git clone https://github.com/milla-jovovich/mempalace.git +cd mempalace +``` + +2. Install the Python package: + +```bash +pip install -e . +``` + +3. The `.codex-plugin` directory is already in the repo root. Codex CLI will detect it automatically when you run Codex from inside the repository. + +4. Initialize your palace: + +```bash +codex /init +``` + +## Available Skills + +| Skill | Description | +|-------|-------------| +| `/help` | Show available commands and usage tips | +| `/init` | Initialize a new memory palace | +| `/search` | Semantic search across all mined memories | +| `/mine` | Mine a project or conversation into your palace | +| `/status` | Show palace status, room counts, and health | + +## Hooks + +The plugin includes an auto-save hook that runs on session stop, automatically preserving conversation context into your palace. + +## Support + +- Repository: https://github.com/milla-jovovich/mempalace +- Issues: https://github.com/milla-jovovich/mempalace/issues diff --git a/.codex-plugin/hooks.json b/.codex-plugin/hooks.json new file mode 100644 index 0000000..1c235f2 --- /dev/null +++ b/.codex-plugin/hooks.json @@ -0,0 +1,37 @@ +{ + "hooks": { + "SessionStart": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "./hooks/mempal-session-start-hook.sh" + } + ] + } + ], + "Stop": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "./hooks/mempal-stop-hook.sh" + } + ] + } + ], + "PreCompact": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "./hooks/mempal-precompact-hook.sh" + } + ] + } + ] + } +} diff --git a/.codex-plugin/hooks/mempal-precompact-hook.sh b/.codex-plugin/hooks/mempal-precompact-hook.sh new file mode 100644 index 0000000..46af49d --- /dev/null +++ b/.codex-plugin/hooks/mempal-precompact-hook.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail +PLUGIN_ROOT="${CODEX_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" + +# Capture stdin (hook input from Codex) +INPUT_FILE=$(mktemp 2>/dev/null || echo "/tmp/mempal-precompact-hook-$$.json") +cat > "$INPUT_FILE" + +# Pipe to Python CLI with codex harness +cat "$INPUT_FILE" | python3 -m mempalace hook run --hook precompact --harness codex +EXIT_CODE=$? + +# Cleanup +rm -f "$INPUT_FILE" 2>/dev/null +exit $EXIT_CODE diff --git a/.codex-plugin/hooks/mempal-session-start-hook.sh b/.codex-plugin/hooks/mempal-session-start-hook.sh new file mode 100644 index 0000000..7c7b5c2 --- /dev/null +++ b/.codex-plugin/hooks/mempal-session-start-hook.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail +PLUGIN_ROOT="${CODEX_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" + +# Capture stdin (hook input from Codex) +INPUT_FILE=$(mktemp 2>/dev/null || echo "/tmp/mempal-session-start-hook-$$.json") +cat > "$INPUT_FILE" + +# Pipe to Python CLI with codex harness +cat "$INPUT_FILE" | python3 -m mempalace hook run --hook session-start --harness codex +EXIT_CODE=$? + +# Cleanup +rm -f "$INPUT_FILE" 2>/dev/null +exit $EXIT_CODE diff --git a/.codex-plugin/hooks/mempal-stop-hook.sh b/.codex-plugin/hooks/mempal-stop-hook.sh new file mode 100644 index 0000000..2d38932 --- /dev/null +++ b/.codex-plugin/hooks/mempal-stop-hook.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail +PLUGIN_ROOT="${CODEX_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" + +# Capture stdin (hook input from Codex) +INPUT_FILE=$(mktemp 2>/dev/null || echo "/tmp/mempal-stop-hook-$$.json") +cat > "$INPUT_FILE" + +# Pipe to Python CLI with codex harness +cat "$INPUT_FILE" | python3 -m mempalace hook run --hook stop --harness codex +EXIT_CODE=$? + +# Cleanup +rm -f "$INPUT_FILE" 2>/dev/null +exit $EXIT_CODE diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 0000000..f040014 --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,35 @@ +{ + "name": "mempalace", + "version": "3.0.0", + "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", + "author": { "name": "milla-jovovich" }, + "homepage": "https://github.com/milla-jovovich/mempalace", + "repository": "https://github.com/milla-jovovich/mempalace", + "license": "MIT", + "keywords": ["memory", "ai", "rag", "mcp", "chromadb", "palace", "search"], + "skills": "./skills/", + "hooks": "./hooks.json", + "mcpServers": { + "mempalace": { + "command": "python3", + "args": ["-m", "mempalace.mcp_server"] + } + }, + "interface": { + "displayName": "MemPalace", + "shortDescription": "AI memory system for Codex", + "longDescription": "Give your AI a persistent memory — mine projects and conversations into a searchable palace backed by ChromaDB, with 19 MCP tools, auto-save hooks, and guided skills.", + "developerName": "milla-jovovich", + "category": "Coding", + "capabilities": ["Interactive", "Read", "Write"], + "websiteURL": "https://github.com/milla-jovovich/mempalace", + "privacyPolicyURL": "https://github.com/milla-jovovich/mempalace", + "termsOfServiceURL": "https://github.com/milla-jovovich/mempalace", + "defaultPrompt": [ + "Search my memories for recent decisions", + "Mine this project into my memory palace", + "Show my palace status and room counts" + ], + "brandColor": "#7C3AED" + } +} diff --git a/.codex-plugin/skills/help/SKILL.md b/.codex-plugin/skills/help/SKILL.md new file mode 100644 index 0000000..d0e8f43 --- /dev/null +++ b/.codex-plugin/skills/help/SKILL.md @@ -0,0 +1,13 @@ +--- +name: help +description: Show MemPalace help — available commands, usage tips, and getting started guidance. +allowed-tools: Bash, Read +--- + +# MemPalace Help + +Run the following command and follow the returned instructions step by step: + +```bash +mempalace instructions help +``` diff --git a/.codex-plugin/skills/init/SKILL.md b/.codex-plugin/skills/init/SKILL.md new file mode 100644 index 0000000..cc0e2f9 --- /dev/null +++ b/.codex-plugin/skills/init/SKILL.md @@ -0,0 +1,13 @@ +--- +name: init +description: Initialize a new MemPalace — guided setup for your AI memory palace with ChromaDB backend. +allowed-tools: Bash, Read, Write, Edit +--- + +# MemPalace Init + +Run the following command and follow the returned instructions step by step: + +```bash +mempalace instructions init +``` diff --git a/.codex-plugin/skills/mine/SKILL.md b/.codex-plugin/skills/mine/SKILL.md new file mode 100644 index 0000000..1a94e29 --- /dev/null +++ b/.codex-plugin/skills/mine/SKILL.md @@ -0,0 +1,13 @@ +--- +name: mine +description: Mine a project or conversation into your MemPalace — extract and store memories for later retrieval. +allowed-tools: Bash, Read, Glob, Grep +--- + +# MemPalace Mine + +Run the following command and follow the returned instructions step by step: + +```bash +mempalace instructions mine +``` diff --git a/.codex-plugin/skills/search/SKILL.md b/.codex-plugin/skills/search/SKILL.md new file mode 100644 index 0000000..4d5bf4b --- /dev/null +++ b/.codex-plugin/skills/search/SKILL.md @@ -0,0 +1,13 @@ +--- +name: search +description: Search your MemPalace — semantic search across all mined memories, projects, and conversations. +allowed-tools: Bash, Read +--- + +# MemPalace Search + +Run the following command and follow the returned instructions step by step: + +```bash +mempalace instructions search +``` diff --git a/.codex-plugin/skills/status/SKILL.md b/.codex-plugin/skills/status/SKILL.md new file mode 100644 index 0000000..617d3be --- /dev/null +++ b/.codex-plugin/skills/status/SKILL.md @@ -0,0 +1,13 @@ +--- +name: status +description: Show MemPalace status — room counts, storage usage, and palace health. +allowed-tools: Bash, Read +--- + +# MemPalace Status + +Run the following command and follow the returned instructions step by step: + +```bash +mempalace instructions status +``` diff --git a/.github/workflows/bump-plugin-version.yml b/.github/workflows/bump-plugin-version.yml index d91c026..43adc8b 100644 --- a/.github/workflows/bump-plugin-version.yml +++ b/.github/workflows/bump-plugin-version.yml @@ -32,11 +32,15 @@ jobs: run: | jq --arg v "${{ steps.version.outputs.version }}" '.plugins[0].version = $v' .claude-plugin/marketplace.json > tmp.json && mv tmp.json .claude-plugin/marketplace.json + - name: Sync codex plugin.json + run: | + jq --arg v "${{ steps.version.outputs.version }}" '.version = $v' .codex-plugin/plugin.json > tmp.json && mv tmp.json .codex-plugin/plugin.json + - name: Commit and push run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add mempalace/version.py .claude-plugin/plugin.json .claude-plugin/marketplace.json + git add mempalace/version.py .claude-plugin/plugin.json .claude-plugin/marketplace.json .codex-plugin/plugin.json if ! git diff --staged --quiet; then git commit -m "chore: bump version to ${{ steps.version.outputs.version }}" git push diff --git a/mempalace/cli.py b/mempalace/cli.py index 9d2465b..0a24abf 100644 --- a/mempalace/cli.py +++ b/mempalace/cli.py @@ -475,13 +475,13 @@ def main(): p_hook_run.add_argument( "--hook", required=True, - choices=["stop", "precompact"], + choices=["session-start", "stop", "precompact"], help="Hook name to run", ) p_hook_run.add_argument( "--harness", required=True, - choices=["claude-code"], + choices=["claude-code", "codex"], help="Harness type (determines stdin JSON format)", ) diff --git a/mempalace/hooks_cli.py b/mempalace/hooks_cli.py index 6e0ec0a..6b4bb0f 100644 --- a/mempalace/hooks_cli.py +++ b/mempalace/hooks_cli.py @@ -1,9 +1,9 @@ """ -Hook logic for MemPalace — Python implementation of stop and precompact hooks. +Hook logic for MemPalace — Python implementation of session-start, stop, and precompact hooks. Reads JSON from stdin, outputs JSON to stdout. -Supported hooks: stop, precompact -Supported harnesses: claude-code (extensible to cursor, gemini, etc.) +Supported hooks: session-start, stop, precompact +Supported harnesses: claude-code, codex (extensible to cursor, gemini, etc.) """ import json @@ -105,10 +105,20 @@ def _parse_claude_code_input(data: dict) -> dict: } +def _parse_codex_input(data: dict) -> dict: + """Parse stdin JSON for the codex harness.""" + return { + "session_id": _sanitize_session_id(str(data.get("session_id", "unknown"))), + "stop_hook_active": data.get("stop_hook_active", False), + "transcript_path": str(data.get("transcript_path", "")), + } + + def _parse_harness_input(data: dict, harness: str) -> dict: """Parse stdin JSON according to the harness type.""" parsers = { "claude-code": _parse_claude_code_input, + "codex": _parse_codex_input, } parser = parsers.get(harness) if parser is None: @@ -163,6 +173,20 @@ def hook_stop(data: dict, harness: str): _output({}) +def hook_session_start(data: dict, harness: str): + """Session start hook: initialize session tracking state.""" + parsed = _parse_harness_input(data, harness) + session_id = parsed["session_id"] + + _log(f"SESSION START for session {session_id}") + + # Initialize session state directory + STATE_DIR.mkdir(parents=True, exist_ok=True) + + # Pass through — no blocking on session start + _output({}) + + def hook_precompact(data: dict, harness: str): """Precompact hook: always block with comprehensive save instruction.""" parsed = _parse_harness_input(data, harness) @@ -196,6 +220,7 @@ def run_hook(hook_name: str, harness: str): data = {} hooks = { + "session-start": hook_session_start, "stop": hook_stop, "precompact": hook_precompact, } From b23210831440969e821ae83963df39f5423deb1e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 16:10:56 +0000 Subject: [PATCH 17/38] chore: bump version to 3.0.3 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 27 ++++++++++++++++++++++----- mempalace/version.py | 2 +- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 4ee6b5f..3d45661 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "mempalace", "source": "./.claude-plugin", "description": "AI memory system — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, guided setup.", - "version": "3.0.2", + "version": "3.0.3", "author": { "name": "milla-jovovich" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 488f6f3..f0a3505 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.2", + "version": "3.0.3", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index f040014..586141c 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,18 +1,31 @@ { "name": "mempalace", - "version": "3.0.0", + "version": "3.0.3", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", - "author": { "name": "milla-jovovich" }, + "author": { + "name": "milla-jovovich" + }, "homepage": "https://github.com/milla-jovovich/mempalace", "repository": "https://github.com/milla-jovovich/mempalace", "license": "MIT", - "keywords": ["memory", "ai", "rag", "mcp", "chromadb", "palace", "search"], + "keywords": [ + "memory", + "ai", + "rag", + "mcp", + "chromadb", + "palace", + "search" + ], "skills": "./skills/", "hooks": "./hooks.json", "mcpServers": { "mempalace": { "command": "python3", - "args": ["-m", "mempalace.mcp_server"] + "args": [ + "-m", + "mempalace.mcp_server" + ] } }, "interface": { @@ -21,7 +34,11 @@ "longDescription": "Give your AI a persistent memory — mine projects and conversations into a searchable palace backed by ChromaDB, with 19 MCP tools, auto-save hooks, and guided skills.", "developerName": "milla-jovovich", "category": "Coding", - "capabilities": ["Interactive", "Read", "Write"], + "capabilities": [ + "Interactive", + "Read", + "Write" + ], "websiteURL": "https://github.com/milla-jovovich/mempalace", "privacyPolicyURL": "https://github.com/milla-jovovich/mempalace", "termsOfServiceURL": "https://github.com/milla-jovovich/mempalace", diff --git a/mempalace/version.py b/mempalace/version.py index dba0e3f..065663a 100644 --- a/mempalace/version.py +++ b/mempalace/version.py @@ -1,3 +1,3 @@ """Single source of truth for the MemPalace package version.""" -__version__ = "3.0.2" +__version__ = "3.0.3" From 8ac0026d2468dd198a4a66dbb1c71a3fb5ec052a Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Wed, 8 Apr 2026 19:15:25 +0300 Subject: [PATCH 18/38] fix: update command descriptions to specify 'generic mempalace skill' --- .claude-plugin/commands/help.md | 2 +- .claude-plugin/commands/init.md | 2 +- .claude-plugin/commands/mine.md | 2 +- .claude-plugin/commands/search.md | 2 +- .claude-plugin/commands/status.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/commands/help.md b/.claude-plugin/commands/help.md index d1c1418..2f56339 100644 --- a/.claude-plugin/commands/help.md +++ b/.claude-plugin/commands/help.md @@ -3,4 +3,4 @@ description: Show comprehensive MemPalace help — available skills, MCP tools, allowed-tools: Bash, Read --- -Invoke the mempalace skill (using the Skill tool) with the `help` command, then follow its instructions. +Invoke the generic mempalace skill (using the Skill tool) with the `help` command, then follow its instructions. diff --git a/.claude-plugin/commands/init.md b/.claude-plugin/commands/init.md index a96a64a..ff27562 100644 --- a/.claude-plugin/commands/init.md +++ b/.claude-plugin/commands/init.md @@ -3,4 +3,4 @@ description: Set up MemPalace — install the package, initialize a palace, conf allowed-tools: Bash, Read, Write, Edit, Glob, Grep --- -Invoke the mempalace skill (using the Skill tool) with the `init` command, then follow its instructions. +Invoke the generic mempalace skill (using the Skill tool) with the `init` command, then follow its instructions. diff --git a/.claude-plugin/commands/mine.md b/.claude-plugin/commands/mine.md index 6fa554b..edac2b0 100644 --- a/.claude-plugin/commands/mine.md +++ b/.claude-plugin/commands/mine.md @@ -4,4 +4,4 @@ argument-hint: Path to project or conversation export to mine. allowed-tools: Bash, Read, Write, Edit, Glob, Grep --- -Invoke the mempalace skill (using the Skill tool) with the `mine` command, then follow its instructions. +Invoke the generic mempalace skill (using the Skill tool) with the `mine` command, then follow its instructions. diff --git a/.claude-plugin/commands/search.md b/.claude-plugin/commands/search.md index 092d8bb..9fe8c34 100644 --- a/.claude-plugin/commands/search.md +++ b/.claude-plugin/commands/search.md @@ -4,4 +4,4 @@ argument-hint: Search query, optionally with wing/room filters. allowed-tools: Bash, Read --- -Invoke the mempalace skill (using the Skill tool) with the `search` command, then follow its instructions. +Invoke the generic mempalace skill (using the Skill tool) with the `search` command, then follow its instructions. diff --git a/.claude-plugin/commands/status.md b/.claude-plugin/commands/status.md index 66732e0..a87f27b 100644 --- a/.claude-plugin/commands/status.md +++ b/.claude-plugin/commands/status.md @@ -3,4 +3,4 @@ description: Show the current state of your memory palace — wings, rooms, draw allowed-tools: Bash, Read --- -Invoke the mempalace skill (using the Skill tool) with the `status` command, then follow its instructions. +Invoke the generic mempalace skill (using the Skill tool) with the `status` command, then follow its instructions. From 4de5229f10bbf9d31a988feeeecd52f5df4a1c8d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 16:15:37 +0000 Subject: [PATCH 19/38] chore: bump version to 3.0.4 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- mempalace/version.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 3d45661..25173c3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "mempalace", "source": "./.claude-plugin", "description": "AI memory system — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, guided setup.", - "version": "3.0.3", + "version": "3.0.4", "author": { "name": "milla-jovovich" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index f0a3505..2176807 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.3", + "version": "3.0.4", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 586141c..5d4fefc 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.3", + "version": "3.0.4", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/mempalace/version.py b/mempalace/version.py index 065663a..30d60af 100644 --- a/mempalace/version.py +++ b/mempalace/version.py @@ -1,3 +1,3 @@ """Single source of truth for the MemPalace package version.""" -__version__ = "3.0.3" +__version__ = "3.0.4" From 34c8f8c1b8e70178c15e335a9729c9cf9d0035a1 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Wed, 8 Apr 2026 19:33:16 +0300 Subject: [PATCH 20/38] fix: update README for marketplace installation and improve hooks_cli file encoding --- .claude-plugin/README.md | 9 ++------- mempalace/hooks_cli.py | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md index 6754626..0212980 100644 --- a/.claude-plugin/README.md +++ b/.claude-plugin/README.md @@ -11,13 +11,8 @@ A Claude Code plugin that gives your AI a persistent memory system. Mine project ### Claude Code Marketplace ```bash -claude plugin add mempalace -``` - -### Git - -```bash -claude plugin add --git https://github.com/milla-jovovich/mempalace +claude plugin marketplace add milla-jovovich/mempalace +claude plugin install --scope user mempalace ``` ### Local Clone diff --git a/mempalace/hooks_cli.py b/mempalace/hooks_cli.py index 6b4bb0f..7d46d1a 100644 --- a/mempalace/hooks_cli.py +++ b/mempalace/hooks_cli.py @@ -46,7 +46,7 @@ def _count_human_messages(transcript_path: str) -> int: return 0 count = 0 try: - with open(path) as f: + with open(path, encoding="utf-8", errors="replace") as f: for line in f: try: entry = json.loads(line) From 1e251cbf36ec74bc8e9c9dffc3a1a2e243a6001d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 16:33:29 +0000 Subject: [PATCH 21/38] chore: bump version to 3.0.5 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- mempalace/version.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 25173c3..aba08d2 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "mempalace", "source": "./.claude-plugin", "description": "AI memory system — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, guided setup.", - "version": "3.0.4", + "version": "3.0.5", "author": { "name": "milla-jovovich" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 2176807..d5d2dce 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.4", + "version": "3.0.5", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 5d4fefc..09565c8 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.4", + "version": "3.0.5", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/mempalace/version.py b/mempalace/version.py index 30d60af..e4c9989 100644 --- a/mempalace/version.py +++ b/mempalace/version.py @@ -1,3 +1,3 @@ """Single source of truth for the MemPalace package version.""" -__version__ = "3.0.4" +__version__ = "3.0.5" From 1888b5688340e7c26f3d1d719f51910371c2ad16 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Wed, 8 Apr 2026 20:00:16 +0300 Subject: [PATCH 22/38] chore: bump version to 3.0.4 in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4862873..98a4bd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mempalace" -version = "3.0.0" +version = "3.0.4" description = "Give your AI a memory — mine projects and conversations into a searchable palace. No API key required." readme = "README.md" requires-python = ">=3.9" From 019d852707205ec394ce5b0d3dcf6a65038b0fa5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 17:00:30 +0000 Subject: [PATCH 23/38] chore: bump version to 3.0.6 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- mempalace/version.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index aba08d2..844276c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "mempalace", "source": "./.claude-plugin", "description": "AI memory system — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, guided setup.", - "version": "3.0.5", + "version": "3.0.6", "author": { "name": "milla-jovovich" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index d5d2dce..e6f6495 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.5", + "version": "3.0.6", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 09565c8..f11a89a 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.5", + "version": "3.0.6", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/mempalace/version.py b/mempalace/version.py index e4c9989..466ad05 100644 --- a/mempalace/version.py +++ b/mempalace/version.py @@ -1,3 +1,3 @@ """Single source of truth for the MemPalace package version.""" -__version__ = "3.0.5" +__version__ = "3.0.6" From 0975b1d23f264f13a0b73e602b66c525414271f1 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Wed, 8 Apr 2026 20:04:10 +0300 Subject: [PATCH 24/38] fix: add syncing of pyproject.toml version during bump process --- .github/workflows/bump-plugin-version.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/bump-plugin-version.yml b/.github/workflows/bump-plugin-version.yml index 43adc8b..0867b3c 100644 --- a/.github/workflows/bump-plugin-version.yml +++ b/.github/workflows/bump-plugin-version.yml @@ -36,11 +36,15 @@ jobs: run: | jq --arg v "${{ steps.version.outputs.version }}" '.version = $v' .codex-plugin/plugin.json > tmp.json && mv tmp.json .codex-plugin/plugin.json + - name: Sync pyproject.toml + run: | + sed -i "s/^version = \".*\"/version = \"${{ steps.version.outputs.version }}\"/" pyproject.toml + - name: Commit and push run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add mempalace/version.py .claude-plugin/plugin.json .claude-plugin/marketplace.json .codex-plugin/plugin.json + git add mempalace/version.py .claude-plugin/plugin.json .claude-plugin/marketplace.json .codex-plugin/plugin.json pyproject.toml if ! git diff --staged --quiet; then git commit -m "chore: bump version to ${{ steps.version.outputs.version }}" git push From 4f1434720c301d3ed8c25b4b0ef91038ea129d15 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 17:04:25 +0000 Subject: [PATCH 25/38] chore: bump version to 3.0.7 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- mempalace/version.py | 2 +- pyproject.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 844276c..8d3dc71 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "mempalace", "source": "./.claude-plugin", "description": "AI memory system — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, guided setup.", - "version": "3.0.6", + "version": "3.0.7", "author": { "name": "milla-jovovich" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index e6f6495..8559c0a 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.6", + "version": "3.0.7", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index f11a89a..807b3b9 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.6", + "version": "3.0.7", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/mempalace/version.py b/mempalace/version.py index 466ad05..0da0a95 100644 --- a/mempalace/version.py +++ b/mempalace/version.py @@ -1,3 +1,3 @@ """Single source of truth for the MemPalace package version.""" -__version__ = "3.0.6" +__version__ = "3.0.7" diff --git a/pyproject.toml b/pyproject.toml index 98a4bd3..7716789 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mempalace" -version = "3.0.4" +version = "3.0.7" description = "Give your AI a memory — mine projects and conversations into a searchable palace. No API key required." readme = "README.md" requires-python = ">=3.9" From c7896d3bb0ab1bef5b551f2242d7bf7a95e6e24e Mon Sep 17 00:00:00 2001 From: Hsu Hsiuwei Date: Thu, 9 Apr 2026 01:15:20 +0800 Subject: [PATCH 26/38] fix: preserve default KG path when --palace not passed When --palace is not explicitly provided, fall back to KnowledgeGraph() which uses DEFAULT_KG_PATH (~/.mempalace/knowledge_graph.sqlite3), preserving backward compatibility for existing users. --- mempalace/mcp_server.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mempalace/mcp_server.py b/mempalace/mcp_server.py index 2ff13b3..7956369 100644 --- a/mempalace/mcp_server.py +++ b/mempalace/mcp_server.py @@ -53,7 +53,10 @@ if _args.palace: os.environ["MEMPALACE_PALACE_PATH"] = os.path.abspath(_args.palace) _config = MempalaceConfig() -_kg = KnowledgeGraph(db_path=os.path.join(_config.palace_path, "knowledge_graph.sqlite3")) +if _args.palace: + _kg = KnowledgeGraph(db_path=os.path.join(_config.palace_path, "knowledge_graph.sqlite3")) +else: + _kg = KnowledgeGraph() def _get_collection(create=False): From e47fa1b5bf3f6bb9e6f6921f042dc2a1022a2e74 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Wed, 8 Apr 2026 20:17:23 +0300 Subject: [PATCH 27/38] refactor: consolidate hook scripts and fixed issue from review --- .codex-plugin/README.md | 2 +- .codex-plugin/hooks.json | 6 +-- .codex-plugin/hooks/mempal-hook.sh | 9 ++++ .codex-plugin/hooks/mempal-precompact-hook.sh | 15 ------- .../hooks/mempal-session-start-hook.sh | 15 ------- .codex-plugin/hooks/mempal-stop-hook.sh | 15 ------- mempalace/hooks_cli.py | 45 ++++++++----------- 7 files changed, 32 insertions(+), 75 deletions(-) create mode 100644 .codex-plugin/hooks/mempal-hook.sh delete mode 100644 .codex-plugin/hooks/mempal-precompact-hook.sh delete mode 100644 .codex-plugin/hooks/mempal-session-start-hook.sh delete mode 100644 .codex-plugin/hooks/mempal-stop-hook.sh diff --git a/.codex-plugin/README.md b/.codex-plugin/README.md index a7ec1de..59e01e9 100644 --- a/.codex-plugin/README.md +++ b/.codex-plugin/README.md @@ -4,7 +4,7 @@ Give your AI a persistent memory -- mine projects and conversations into a searc ## Prerequisites -- Python 3.10+ +- Python 3.9+ - Codex CLI installed and configured - `pip install mempalace` diff --git a/.codex-plugin/hooks.json b/.codex-plugin/hooks.json index 1c235f2..46f7e66 100644 --- a/.codex-plugin/hooks.json +++ b/.codex-plugin/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "./hooks/mempal-session-start-hook.sh" + "command": "${CODEX_PLUGIN_ROOT}/hooks/mempal-hook.sh session-start" } ] } @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "./hooks/mempal-stop-hook.sh" + "command": "${CODEX_PLUGIN_ROOT}/hooks/mempal-hook.sh stop" } ] } @@ -28,7 +28,7 @@ "hooks": [ { "type": "command", - "command": "./hooks/mempal-precompact-hook.sh" + "command": "${CODEX_PLUGIN_ROOT}/hooks/mempal-hook.sh precompact" } ] } diff --git a/.codex-plugin/hooks/mempal-hook.sh b/.codex-plugin/hooks/mempal-hook.sh new file mode 100644 index 0000000..1cc0050 --- /dev/null +++ b/.codex-plugin/hooks/mempal-hook.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail +HOOK_NAME="${1:?Usage: mempal-hook.sh }" +INPUT_FILE=$(mktemp) || { echo "Failed to create temp file" >&2; exit 1; } +cat > "$INPUT_FILE" +cat "$INPUT_FILE" | python3 -m mempalace hook run --hook "$HOOK_NAME" --harness codex +EXIT_CODE=$? +rm -f "$INPUT_FILE" 2>/dev/null +exit $EXIT_CODE diff --git a/.codex-plugin/hooks/mempal-precompact-hook.sh b/.codex-plugin/hooks/mempal-precompact-hook.sh deleted file mode 100644 index 46af49d..0000000 --- a/.codex-plugin/hooks/mempal-precompact-hook.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -PLUGIN_ROOT="${CODEX_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" - -# Capture stdin (hook input from Codex) -INPUT_FILE=$(mktemp 2>/dev/null || echo "/tmp/mempal-precompact-hook-$$.json") -cat > "$INPUT_FILE" - -# Pipe to Python CLI with codex harness -cat "$INPUT_FILE" | python3 -m mempalace hook run --hook precompact --harness codex -EXIT_CODE=$? - -# Cleanup -rm -f "$INPUT_FILE" 2>/dev/null -exit $EXIT_CODE diff --git a/.codex-plugin/hooks/mempal-session-start-hook.sh b/.codex-plugin/hooks/mempal-session-start-hook.sh deleted file mode 100644 index 7c7b5c2..0000000 --- a/.codex-plugin/hooks/mempal-session-start-hook.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -PLUGIN_ROOT="${CODEX_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" - -# Capture stdin (hook input from Codex) -INPUT_FILE=$(mktemp 2>/dev/null || echo "/tmp/mempal-session-start-hook-$$.json") -cat > "$INPUT_FILE" - -# Pipe to Python CLI with codex harness -cat "$INPUT_FILE" | python3 -m mempalace hook run --hook session-start --harness codex -EXIT_CODE=$? - -# Cleanup -rm -f "$INPUT_FILE" 2>/dev/null -exit $EXIT_CODE diff --git a/.codex-plugin/hooks/mempal-stop-hook.sh b/.codex-plugin/hooks/mempal-stop-hook.sh deleted file mode 100644 index 2d38932..0000000 --- a/.codex-plugin/hooks/mempal-stop-hook.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -PLUGIN_ROOT="${CODEX_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" - -# Capture stdin (hook input from Codex) -INPUT_FILE=$(mktemp 2>/dev/null || echo "/tmp/mempal-stop-hook-$$.json") -cat > "$INPUT_FILE" - -# Pipe to Python CLI with codex harness -cat "$INPUT_FILE" | python3 -m mempalace hook run --hook stop --harness codex -EXIT_CODE=$? - -# Cleanup -rm -f "$INPUT_FILE" 2>/dev/null -exit $EXIT_CODE diff --git a/mempalace/hooks_cli.py b/mempalace/hooks_cli.py index 7d46d1a..fe6e4eb 100644 --- a/mempalace/hooks_cli.py +++ b/mempalace/hooks_cli.py @@ -53,8 +53,15 @@ def _count_human_messages(transcript_path: str) -> int: msg = entry.get("message", {}) if isinstance(msg, dict) and msg.get("role") == "user": content = msg.get("content", "") - if isinstance(content, str) and "" in content: - continue + if isinstance(content, str): + if "" in content: + continue + elif isinstance(content, list): + text = " ".join( + b.get("text", "") for b in content if isinstance(b, dict) + ) + if "" in text: + continue count += 1 except (json.JSONDecodeError, AttributeError): pass @@ -96,35 +103,19 @@ def _maybe_auto_ingest(): pass -def _parse_claude_code_input(data: dict) -> dict: - """Parse stdin JSON for the claude-code harness.""" - return { - "session_id": _sanitize_session_id(str(data.get("session_id", "unknown"))), - "stop_hook_active": data.get("stop_hook_active", False), - "transcript_path": str(data.get("transcript_path", "")), - } - - -def _parse_codex_input(data: dict) -> dict: - """Parse stdin JSON for the codex harness.""" - return { - "session_id": _sanitize_session_id(str(data.get("session_id", "unknown"))), - "stop_hook_active": data.get("stop_hook_active", False), - "transcript_path": str(data.get("transcript_path", "")), - } +SUPPORTED_HARNESSES = {"claude-code", "codex"} def _parse_harness_input(data: dict, harness: str) -> dict: """Parse stdin JSON according to the harness type.""" - parsers = { - "claude-code": _parse_claude_code_input, - "codex": _parse_codex_input, - } - parser = parsers.get(harness) - if parser is None: + if harness not in SUPPORTED_HARNESSES: print(f"Unknown harness: {harness}", file=sys.stderr) sys.exit(1) - return parser(data) + return { + "session_id": _sanitize_session_id(str(data.get("session_id", "unknown"))), + "stop_hook_active": data.get("stop_hook_active", False), + "transcript_path": str(data.get("transcript_path", "")), + } def hook_stop(data: dict, harness: str): @@ -135,7 +126,7 @@ def hook_stop(data: dict, harness: str): transcript_path = parsed["transcript_path"] # If already in a save cycle, let through (infinite-loop prevention) - if stop_hook_active in (True, "True", "true"): + if str(stop_hook_active).lower() in ("true", "1", "yes"): _output({}) return @@ -204,6 +195,7 @@ def hook_precompact(data: dict, harness: str): [sys.executable, "-m", "mempalace", "mine", mempal_dir], stdout=log_f, stderr=log_f, + timeout=60, ) except OSError: pass @@ -217,6 +209,7 @@ def run_hook(hook_name: str, harness: str): try: data = json.load(sys.stdin) except (json.JSONDecodeError, EOFError): + _log("WARNING: Failed to parse stdin JSON, proceeding with empty data") data = {} hooks = { From 67e21c582c0b77b703c4d5ef86b36a4f969a1c59 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 17:17:36 +0000 Subject: [PATCH 28/38] chore: bump version to 3.0.8 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- mempalace/version.py | 2 +- pyproject.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 8d3dc71..c0abf8b 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "mempalace", "source": "./.claude-plugin", "description": "AI memory system — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, guided setup.", - "version": "3.0.7", + "version": "3.0.8", "author": { "name": "milla-jovovich" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 8559c0a..648361c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.7", + "version": "3.0.8", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 807b3b9..8892791 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.7", + "version": "3.0.8", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/mempalace/version.py b/mempalace/version.py index 0da0a95..2ac6bef 100644 --- a/mempalace/version.py +++ b/mempalace/version.py @@ -1,3 +1,3 @@ """Single source of truth for the MemPalace package version.""" -__version__ = "3.0.7" +__version__ = "3.0.8" diff --git a/pyproject.toml b/pyproject.toml index 7716789..ff7e2f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mempalace" -version = "3.0.7" +version = "3.0.8" description = "Give your AI a memory — mine projects and conversations into a searchable palace. No API key required." readme = "README.md" requires-python = ">=3.9" From 9de302f881b01ba21c4833666f765d120083dc99 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Wed, 8 Apr 2026 20:40:03 +0300 Subject: [PATCH 29/38] feat: update README and CI configuration, add tests for hooks functionality --- .claude-plugin/README.md | 4 +- .codex-plugin/README.md | 4 +- .github/workflows/ci.yml | 2 +- README.md | 18 ++++ pyproject.toml | 15 ++- tests/test_hooks_cli.py | 192 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 tests/test_hooks_cli.py diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md index 0212980..fd98952 100644 --- a/.claude-plugin/README.md +++ b/.claude-plugin/README.md @@ -43,9 +43,11 @@ After installing the plugin, run the init command to complete setup (pip install MemPalace registers two hooks that run automatically: -- **Stop** -- Saves conversation context when a session ends. +- **Stop** -- Saves conversation context every 15 messages. - **PreCompact** -- Preserves important memories before context compaction. +Set the `MEMPAL_DIR` environment variable to a directory path to automatically run `mempalace mine` on that directory during each save trigger. + ## MCP Server The plugin automatically configures a local MCP server with 19 tools for storing, searching, and managing memories. No manual MCP setup is required -- `/mempalace:init` handles everything. diff --git a/.codex-plugin/README.md b/.codex-plugin/README.md index 59e01e9..57dbc34 100644 --- a/.codex-plugin/README.md +++ b/.codex-plugin/README.md @@ -65,7 +65,9 @@ codex /init ## Hooks -The plugin includes an auto-save hook that runs on session stop, automatically preserving conversation context into your palace. +The plugin includes auto-save hooks that run on session stop (every 15 messages) and before context compaction, automatically preserving conversation context into your palace. + +Set the `MEMPAL_DIR` environment variable to a directory path to automatically run `mempalace mine` on that directory during each save trigger. ## Support diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ccb15e..4cca6ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - run: pip install -e ".[dev]" - - run: python -m pytest tests/ -v + - run: python -m pytest tests/ -v --cov=mempalace --cov-report=term-missing --cov-fail-under=30 lint: runs-on: ubuntu-latest diff --git a/README.md b/README.md index d05952f..441f506 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,17 @@ Three mining modes: **projects** (code and docs), **convos** (conversation expor After the one-time setup (install → init → mine), you don't run MemPalace commands manually. Your AI uses it for you. There are two ways, depending on which AI you use. +### With Claude Code (recommended) + +Native marketplace install: + +```bash +claude plugin marketplace add milla-jovovich/mempalace +claude plugin install --scope user mempalace +``` + +Restart Claude Code, then type `/skills` to verify "mempalace" appears. + ### With Claude, ChatGPT, Cursor, Gemini (MCP-compatible tools) ```bash @@ -439,6 +450,11 @@ Letta charges $20–200/mo for agent-managed memory. MemPalace does it with a wi ## MCP Server ```bash +# Via plugin (recommended) +claude plugin marketplace add milla-jovovich/mempalace +claude plugin install --scope user mempalace + +# Or manually claude mcp add mempalace -- python -m mempalace.mcp_server ``` @@ -509,6 +525,8 @@ Two hooks for Claude Code that automatically save memories during work: } ``` +**Optional auto-ingest:** Set the `MEMPAL_DIR` environment variable to a directory path and the hooks will automatically run `mempalace mine` on that directory during each save trigger (background on stop, synchronous on precompact). + --- ## Benchmarks diff --git a/pyproject.toml b/pyproject.toml index ff7e2f1..a83ace6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,11 +38,11 @@ Repository = "https://github.com/milla-jovovich/mempalace" mempalace = "mempalace:main" [project.optional-dependencies] -dev = ["pytest>=7.0", "ruff>=0.4.0"] +dev = ["pytest>=7.0", "pytest-cov>=4.0", "ruff>=0.4.0"] spellcheck = ["autocorrect>=2.0"] [dependency-groups] -dev = ["pytest>=7.0", "ruff>=0.4.0"] +dev = ["pytest>=7.0", "pytest-cov>=4.0", "ruff>=0.4.0"] [build-system] requires = ["hatchling"] @@ -64,3 +64,14 @@ quote-style = "double" [tool.pytest.ini_options] testpaths = ["tests"] + +[tool.coverage.run] +source = ["mempalace"] + +[tool.coverage.report] +fail_under = 30 +show_missing = true +exclude_lines = [ + "if __name__", + "pragma: no cover", +] diff --git a/tests/test_hooks_cli.py b/tests/test_hooks_cli.py new file mode 100644 index 0000000..8eeffed --- /dev/null +++ b/tests/test_hooks_cli.py @@ -0,0 +1,192 @@ +import contextlib +import json +from pathlib import Path +from unittest.mock import patch + +from mempalace.hooks_cli import ( + SAVE_INTERVAL, + STOP_BLOCK_REASON, + PRECOMPACT_BLOCK_REASON, + _count_human_messages, + _sanitize_session_id, + hook_stop, + hook_session_start, + hook_precompact, +) + + +# --- _sanitize_session_id --- + + +def test_sanitize_normal_id(): + assert _sanitize_session_id("abc-123_XYZ") == "abc-123_XYZ" + + +def test_sanitize_strips_dangerous_chars(): + assert _sanitize_session_id("../../etc/passwd") == "etcpasswd" + + +def test_sanitize_empty_returns_unknown(): + assert _sanitize_session_id("") == "unknown" + assert _sanitize_session_id("!!!") == "unknown" + + +# --- _count_human_messages --- + + +def _write_transcript(path: Path, entries: list[dict]): + with open(path, "w", encoding="utf-8") as f: + for entry in entries: + f.write(json.dumps(entry) + "\n") + + +def test_count_human_messages_basic(tmp_path): + transcript = tmp_path / "t.jsonl" + _write_transcript(transcript, [ + {"message": {"role": "user", "content": "hello"}}, + {"message": {"role": "assistant", "content": "hi"}}, + {"message": {"role": "user", "content": "bye"}}, + ]) + assert _count_human_messages(str(transcript)) == 2 + + +def test_count_skips_command_messages(tmp_path): + transcript = tmp_path / "t.jsonl" + _write_transcript(transcript, [ + {"message": {"role": "user", "content": "status"}}, + {"message": {"role": "user", "content": "real question"}}, + ]) + assert _count_human_messages(str(transcript)) == 1 + + +def test_count_handles_list_content(tmp_path): + transcript = tmp_path / "t.jsonl" + _write_transcript(transcript, [ + {"message": {"role": "user", "content": [{"type": "text", "text": "hello"}]}}, + {"message": {"role": "user", "content": [{"type": "text", "text": "x"}]}}, + ]) + assert _count_human_messages(str(transcript)) == 1 + + +def test_count_missing_file(): + assert _count_human_messages("/nonexistent/path.jsonl") == 0 + + +def test_count_empty_file(tmp_path): + transcript = tmp_path / "t.jsonl" + transcript.write_text("") + assert _count_human_messages(str(transcript)) == 0 + + +def test_count_malformed_json_lines(tmp_path): + transcript = tmp_path / "t.jsonl" + transcript.write_text('not json\n{"message": {"role": "user", "content": "ok"}}\n') + assert _count_human_messages(str(transcript)) == 1 + + +# --- hook_stop --- + + +def _capture_hook_output(hook_fn, data, harness="claude-code", state_dir=None): + """Run a hook and capture its JSON stdout output.""" + import io + buf = io.StringIO() + patches = [patch("mempalace.hooks_cli._output", side_effect=lambda d: buf.write(json.dumps(d)))] + if state_dir: + patches.append(patch("mempalace.hooks_cli.STATE_DIR", state_dir)) + with contextlib.ExitStack() as stack: + for p in patches: + stack.enter_context(p) + hook_fn(data, harness) + return json.loads(buf.getvalue()) + + +def test_stop_hook_passthrough_when_active(tmp_path): + with patch("mempalace.hooks_cli.STATE_DIR", tmp_path): + result = _capture_hook_output( + hook_stop, + {"session_id": "test", "stop_hook_active": True, "transcript_path": ""}, + state_dir=tmp_path, + ) + assert result == {} + + +def test_stop_hook_passthrough_when_active_string(tmp_path): + with patch("mempalace.hooks_cli.STATE_DIR", tmp_path): + result = _capture_hook_output( + hook_stop, + {"session_id": "test", "stop_hook_active": "true", "transcript_path": ""}, + state_dir=tmp_path, + ) + assert result == {} + + +def test_stop_hook_passthrough_below_interval(tmp_path): + transcript = tmp_path / "t.jsonl" + _write_transcript(transcript, [ + {"message": {"role": "user", "content": f"msg {i}"}} + for i in range(SAVE_INTERVAL - 1) + ]) + result = _capture_hook_output( + hook_stop, + {"session_id": "test", "stop_hook_active": False, "transcript_path": str(transcript)}, + state_dir=tmp_path, + ) + assert result == {} + + +def test_stop_hook_blocks_at_interval(tmp_path): + transcript = tmp_path / "t.jsonl" + _write_transcript(transcript, [ + {"message": {"role": "user", "content": f"msg {i}"}} + for i in range(SAVE_INTERVAL) + ]) + result = _capture_hook_output( + hook_stop, + {"session_id": "test", "stop_hook_active": False, "transcript_path": str(transcript)}, + state_dir=tmp_path, + ) + assert result["decision"] == "block" + assert result["reason"] == STOP_BLOCK_REASON + + +def test_stop_hook_tracks_save_point(tmp_path): + transcript = tmp_path / "t.jsonl" + _write_transcript(transcript, [ + {"message": {"role": "user", "content": f"msg {i}"}} + for i in range(SAVE_INTERVAL) + ]) + data = {"session_id": "test", "stop_hook_active": False, "transcript_path": str(transcript)} + + # First call blocks + result = _capture_hook_output(hook_stop, data, state_dir=tmp_path) + assert result["decision"] == "block" + + # Second call with same count passes through (already saved) + result = _capture_hook_output(hook_stop, data, state_dir=tmp_path) + assert result == {} + + +# --- hook_session_start --- + + +def test_session_start_passes_through(tmp_path): + result = _capture_hook_output( + hook_session_start, + {"session_id": "test"}, + state_dir=tmp_path, + ) + assert result == {} + + +# --- hook_precompact --- + + +def test_precompact_always_blocks(tmp_path): + result = _capture_hook_output( + hook_precompact, + {"session_id": "test"}, + state_dir=tmp_path, + ) + assert result["decision"] == "block" + assert result["reason"] == PRECOMPACT_BLOCK_REASON From 43cf87315e9b6ee988f19d6d4c7e8a562c08c264 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 17:40:17 +0000 Subject: [PATCH 30/38] chore: bump version to 3.0.9 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- mempalace/version.py | 2 +- pyproject.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c0abf8b..b2b7ed2 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "mempalace", "source": "./.claude-plugin", "description": "AI memory system — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, guided setup.", - "version": "3.0.8", + "version": "3.0.9", "author": { "name": "milla-jovovich" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 648361c..a5d97af 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.8", + "version": "3.0.9", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 8892791..fb19f29 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.8", + "version": "3.0.9", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/mempalace/version.py b/mempalace/version.py index 2ac6bef..0a33c87 100644 --- a/mempalace/version.py +++ b/mempalace/version.py @@ -1,3 +1,3 @@ """Single source of truth for the MemPalace package version.""" -__version__ = "3.0.8" +__version__ = "3.0.9" diff --git a/pyproject.toml b/pyproject.toml index a83ace6..c19104a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mempalace" -version = "3.0.8" +version = "3.0.9" description = "Give your AI a memory — mine projects and conversations into a searchable palace. No API key required." readme = "README.md" requires-python = ">=3.9" From 2eb4d43a5969a47c396dec7dc7e36e295cac07f4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 17:41:55 +0000 Subject: [PATCH 31/38] chore: bump version to 3.0.10 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- mempalace/version.py | 2 +- pyproject.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b2b7ed2..e1b261b 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "mempalace", "source": "./.claude-plugin", "description": "AI memory system — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, guided setup.", - "version": "3.0.9", + "version": "3.0.10", "author": { "name": "milla-jovovich" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a5d97af..2073096 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.9", + "version": "3.0.10", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index fb19f29..db27f47 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.9", + "version": "3.0.10", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/mempalace/version.py b/mempalace/version.py index 0a33c87..9db27c6 100644 --- a/mempalace/version.py +++ b/mempalace/version.py @@ -1,3 +1,3 @@ """Single source of truth for the MemPalace package version.""" -__version__ = "3.0.9" +__version__ = "3.0.10" diff --git a/pyproject.toml b/pyproject.toml index c19104a..3755b48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mempalace" -version = "3.0.9" +version = "3.0.10" description = "Give your AI a memory — mine projects and conversations into a searchable palace. No API key required." readme = "README.md" requires-python = ">=3.9" From fcc9ce84f2dd4ce27cb91d437d68e96a0f2c59bf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 17:46:56 +0000 Subject: [PATCH 32/38] chore: bump version to 3.0.11 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- mempalace/version.py | 2 +- pyproject.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e1b261b..0794c95 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "mempalace", "source": "./.claude-plugin", "description": "AI memory system — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, guided setup.", - "version": "3.0.10", + "version": "3.0.11", "author": { "name": "milla-jovovich" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 2073096..295b247 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.10", + "version": "3.0.11", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index db27f47..480a21f 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.10", + "version": "3.0.11", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/mempalace/version.py b/mempalace/version.py index 9db27c6..6bf70fc 100644 --- a/mempalace/version.py +++ b/mempalace/version.py @@ -1,3 +1,3 @@ """Single source of truth for the MemPalace package version.""" -__version__ = "3.0.10" +__version__ = "3.0.11" diff --git a/pyproject.toml b/pyproject.toml index 3755b48..00e5c93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mempalace" -version = "3.0.10" +version = "3.0.11" description = "Give your AI a memory — mine projects and conversations into a searchable palace. No API key required." readme = "README.md" requires-python = ">=3.9" From a4149ab248c5aa6c314eec5e4ebaa9eaef55c29c Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:27:41 -0300 Subject: [PATCH 33/38] fix: use upsert and deterministic IDs to prevent data stagnation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MCP tool_add_drawer: - Make drawer_id content-based: hash full content instead of content[:100] + timestamp. Same content → same ID, eliminating TOCTOU race conditions - Switch from col.add() to col.upsert() so re-filing with updated content updates the existing drawer miner.add_drawer: - Switch from collection.add() to collection.upsert() so re-mining a modified file updates instead of silently failing - Remove the try/except catching 'already exists' — upsert handles this naturally Findings: #11 (HIGH — add ignores updates), #6 (MEDIUM — TOCTOU), #13 (MEDIUM — non-deterministic IDs) Includes test infrastructure from PR #131. 92 tests pass. --- mempalace/mcp_server.py | 4 ++-- mempalace/miner.py | 6 ++---- tests/test_knowledge_graph.py | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/mempalace/mcp_server.py b/mempalace/mcp_server.py index b447249..bda4c1a 100644 --- a/mempalace/mcp_server.py +++ b/mempalace/mcp_server.py @@ -292,10 +292,10 @@ def tool_add_drawer( "matches": dup["matches"], } - drawer_id = f"drawer_{wing}_{room}_{hashlib.md5((content[:100] + datetime.now().isoformat()).encode()).hexdigest()[:16]}" + drawer_id = f"drawer_{wing}_{room}_{hashlib.md5(content.encode()).hexdigest()[:16]}" try: - col.add( + col.upsert( ids=[drawer_id], documents=[content], metadatas=[ diff --git a/mempalace/miner.py b/mempalace/miner.py index 7b4e949..a53cf76 100644 --- a/mempalace/miner.py +++ b/mempalace/miner.py @@ -417,7 +417,7 @@ def add_drawer( """Add one drawer to the palace.""" drawer_id = f"drawer_{wing}_{room}_{hashlib.md5((source_file + str(chunk_index)).encode(), usedforsecurity=False).hexdigest()[:16]}" try: - collection.add( + collection.upsert( documents=[content], ids=[drawer_id], metadatas=[ @@ -432,9 +432,7 @@ def add_drawer( ], ) return True - except Exception as e: - if "already exists" in str(e).lower() or "duplicate" in str(e).lower(): - return False + except Exception: raise diff --git a/tests/test_knowledge_graph.py b/tests/test_knowledge_graph.py index d7d9838..535eace 100644 --- a/tests/test_knowledge_graph.py +++ b/tests/test_knowledge_graph.py @@ -6,6 +6,7 @@ timeline, stats, and edge cases (duplicate triples, ID collisions). """ + class TestEntityOperations: def test_add_entity(self, kg): eid = kg.add_entity("Alice", entity_type="person") @@ -124,7 +125,6 @@ class TestWALMode: conn.close() assert mode == "wal" - class TestStats: def test_stats_empty(self, kg): stats = kg.stats() From bf88daa649f9c8db915d5e6212df369ab65cd6b6 Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:44:19 -0300 Subject: [PATCH 34/38] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20re-?= =?UTF-8?q?mine=20modified=20files,=20idempotent=20add=5Fdrawer,=20cleanup?= =?UTF-8?q?=20ChromaDB=20handles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mempalace/mcp_server.py | 17 ++++----- mempalace/miner.py | 40 ++++++++++++++------ tests/conftest.py | 4 +- tests/test_mcp_server.py | 81 ++++++++++++++++++++-------------------- 4 files changed, 79 insertions(+), 63 deletions(-) diff --git a/mempalace/mcp_server.py b/mempalace/mcp_server.py index bda4c1a..dcaff62 100644 --- a/mempalace/mcp_server.py +++ b/mempalace/mcp_server.py @@ -283,17 +283,16 @@ def tool_add_drawer( if not col: return _no_palace() - # Duplicate check - dup = tool_check_duplicate(content, threshold=0.9) - if dup.get("is_duplicate"): - return { - "success": False, - "reason": "duplicate", - "matches": dup["matches"], - } - drawer_id = f"drawer_{wing}_{room}_{hashlib.md5(content.encode()).hexdigest()[:16]}" + # Idempotency: if the deterministic ID already exists, return success as a no-op. + try: + existing = col.get(ids=[drawer_id]) + if existing and existing["ids"]: + return {"success": True, "reason": "already_exists", "drawer_id": drawer_id} + except Exception: + pass + try: col.upsert( ids=[drawer_id], diff --git a/mempalace/miner.py b/mempalace/miner.py index a53cf76..e29fb25 100644 --- a/mempalace/miner.py +++ b/mempalace/miner.py @@ -403,10 +403,22 @@ def get_collection(palace_path: str): def file_already_mined(collection, source_file: str) -> bool: - """Fast check: has this file been filed before?""" + """Fast check: has this file been filed before and is unchanged? + + Compares the stored mtime in drawer metadata against the file's current + mtime. Returns False (needs re-mining) when the file has been modified + since it was last mined, or when no mtime was stored. + """ try: results = collection.get(where={"source_file": source_file}, limit=1) - return len(results.get("ids", [])) > 0 + if not results.get("ids"): + return False + stored_meta = results["metadatas"][0] if results.get("metadatas") else {} + stored_mtime = stored_meta.get("source_mtime") + if stored_mtime is None: + return False + current_mtime = os.path.getmtime(source_file) + return float(stored_mtime) == current_mtime except Exception: return False @@ -417,19 +429,23 @@ def add_drawer( """Add one drawer to the palace.""" drawer_id = f"drawer_{wing}_{room}_{hashlib.md5((source_file + str(chunk_index)).encode(), usedforsecurity=False).hexdigest()[:16]}" try: + metadata = { + "wing": wing, + "room": room, + "source_file": source_file, + "chunk_index": chunk_index, + "added_by": agent, + "filed_at": datetime.now().isoformat(), + } + # Store file mtime so we can detect modifications later. + try: + metadata["source_mtime"] = os.path.getmtime(source_file) + except OSError: + pass collection.upsert( documents=[content], ids=[drawer_id], - metadatas=[ - { - "wing": wing, - "room": room, - "source_file": source_file, - "chunk_index": chunk_index, - "added_by": agent, - "filed_at": datetime.now().isoformat(), - } - ], + metadatas=[metadata], ) return True except Exception: diff --git a/tests/conftest.py b/tests/conftest.py index eb2b432..7a3e55a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -102,7 +102,9 @@ def collection(palace_path): """A ChromaDB collection pre-seeded in the temp palace.""" client = chromadb.PersistentClient(path=palace_path) col = client.get_or_create_collection("mempalace_drawers") - return col + yield col + client.delete_collection("mempalace_drawers") + del client @pytest.fixture diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index 09a3c46..aff9df3 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -9,25 +9,26 @@ via monkeypatch to avoid touching real data. import json -def _patch_mcp_server(monkeypatch, config, palace_path, kg): +def _patch_mcp_server(monkeypatch, config, kg): """Patch the mcp_server module globals to use test fixtures.""" from mempalace import mcp_server - assert getattr(config, "palace_path", None) == palace_path, ( - f"config.palace_path ({getattr(config, 'palace_path', None)!r}) does not match palace_path fixture ({palace_path!r})" - ) monkeypatch.setattr(mcp_server, "_config", config) monkeypatch.setattr(mcp_server, "_kg", kg) def _get_collection(palace_path, create=False): - """Helper to get collection from test palace.""" + """Helper to get collection from test palace. + + Returns (client, collection) so callers can clean up the client + when they are done. + """ import chromadb client = chromadb.PersistentClient(path=palace_path) if create: - return client.get_or_create_collection("mempalace_drawers") - return client.get_collection("mempalace_drawers") + return client, client.get_or_create_collection("mempalace_drawers") + return client, client.get_collection("mempalace_drawers") # ── Protocol Layer ────────────────────────────────────────────────────── @@ -77,11 +78,11 @@ class TestHandleRequest: assert resp["error"]["code"] == -32601 def test_tools_call_dispatches(self, monkeypatch, config, palace_path, seeded_kg): - _patch_mcp_server(monkeypatch, config, palace_path, seeded_kg) + _patch_mcp_server(monkeypatch, config, seeded_kg) from mempalace.mcp_server import handle_request # Create a collection so status works - _get_collection(palace_path, create=True) + _client, _col = _get_collection(palace_path, create=True); del _client resp = handle_request( { @@ -100,8 +101,8 @@ class TestHandleRequest: class TestReadTools: def test_status_empty_palace(self, monkeypatch, config, palace_path, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) - _get_collection(palace_path, create=True) + _patch_mcp_server(monkeypatch, config, kg) + _client, _col = _get_collection(palace_path, create=True); del _client from mempalace.mcp_server import tool_status result = tool_status() @@ -109,7 +110,7 @@ class TestReadTools: assert result["wings"] == {} def test_status_with_data(self, monkeypatch, config, palace_path, seeded_collection, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) + _patch_mcp_server(monkeypatch, config, kg) from mempalace.mcp_server import tool_status result = tool_status() @@ -118,7 +119,7 @@ class TestReadTools: assert "notes" in result["wings"] def test_list_wings(self, monkeypatch, config, palace_path, seeded_collection, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) + _patch_mcp_server(monkeypatch, config, kg) from mempalace.mcp_server import tool_list_wings result = tool_list_wings() @@ -126,7 +127,7 @@ class TestReadTools: assert result["wings"]["notes"] == 1 def test_list_rooms_all(self, monkeypatch, config, palace_path, seeded_collection, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) + _patch_mcp_server(monkeypatch, config, kg) from mempalace.mcp_server import tool_list_rooms result = tool_list_rooms() @@ -135,7 +136,7 @@ class TestReadTools: assert "planning" in result["rooms"] def test_list_rooms_filtered(self, monkeypatch, config, palace_path, seeded_collection, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) + _patch_mcp_server(monkeypatch, config, kg) from mempalace.mcp_server import tool_list_rooms result = tool_list_rooms(wing="project") @@ -143,7 +144,7 @@ class TestReadTools: assert "planning" not in result["rooms"] def test_get_taxonomy(self, monkeypatch, config, palace_path, seeded_collection, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) + _patch_mcp_server(monkeypatch, config, kg) from mempalace.mcp_server import tool_get_taxonomy result = tool_get_taxonomy() @@ -151,10 +152,8 @@ class TestReadTools: assert result["taxonomy"]["project"]["frontend"] == 1 assert result["taxonomy"]["notes"]["planning"] == 1 - def test_no_palace_returns_error(self, monkeypatch, config, kg, tmp_path): - missing = str(tmp_path / "missing") - config._file_config["palace_path"] = missing - _patch_mcp_server(monkeypatch, config, missing, kg) + def test_no_palace_returns_error(self, monkeypatch, config, kg): + _patch_mcp_server(monkeypatch, config, kg) from mempalace.mcp_server import tool_status result = tool_status() @@ -166,7 +165,7 @@ class TestReadTools: class TestSearchTool: def test_search_basic(self, monkeypatch, config, palace_path, seeded_collection, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) + _patch_mcp_server(monkeypatch, config, kg) from mempalace.mcp_server import tool_search result = tool_search(query="JWT authentication tokens") @@ -177,14 +176,14 @@ class TestSearchTool: assert "JWT" in top["text"] or "authentication" in top["text"].lower() def test_search_with_wing_filter(self, monkeypatch, config, palace_path, seeded_collection, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) + _patch_mcp_server(monkeypatch, config, kg) from mempalace.mcp_server import tool_search result = tool_search(query="planning", wing="notes") assert all(r["wing"] == "notes" for r in result["results"]) def test_search_with_room_filter(self, monkeypatch, config, palace_path, seeded_collection, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) + _patch_mcp_server(monkeypatch, config, kg) from mempalace.mcp_server import tool_search result = tool_search(query="database", room="backend") @@ -196,8 +195,8 @@ class TestSearchTool: class TestWriteTools: def test_add_drawer(self, monkeypatch, config, palace_path, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) - _get_collection(palace_path, create=True) + _patch_mcp_server(monkeypatch, config, kg) + _client, _col = _get_collection(palace_path, create=True); del _client from mempalace.mcp_server import tool_add_drawer result = tool_add_drawer( @@ -211,8 +210,8 @@ class TestWriteTools: assert result["drawer_id"].startswith("drawer_test_wing_test_room_") def test_add_drawer_duplicate_detection(self, monkeypatch, config, palace_path, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) - _get_collection(palace_path, create=True) + _patch_mcp_server(monkeypatch, config, kg) + _client, _col = _get_collection(palace_path, create=True); del _client from mempalace.mcp_server import tool_add_drawer content = "This is a unique test memory about Rust ownership and borrowing." @@ -220,11 +219,11 @@ class TestWriteTools: assert result1["success"] is True result2 = tool_add_drawer(wing="w", room="r", content=content) - assert result2["success"] is False - assert result2["reason"] == "duplicate" + assert result2["success"] is True + assert result2["reason"] == "already_exists" def test_delete_drawer(self, monkeypatch, config, palace_path, seeded_collection, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) + _patch_mcp_server(monkeypatch, config, kg) from mempalace.mcp_server import tool_delete_drawer result = tool_delete_drawer("drawer_proj_backend_aaa") @@ -232,14 +231,14 @@ class TestWriteTools: assert seeded_collection.count() == 3 def test_delete_drawer_not_found(self, monkeypatch, config, palace_path, seeded_collection, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) + _patch_mcp_server(monkeypatch, config, kg) from mempalace.mcp_server import tool_delete_drawer result = tool_delete_drawer("nonexistent_drawer") assert result["success"] is False def test_check_duplicate(self, monkeypatch, config, palace_path, seeded_collection, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) + _patch_mcp_server(monkeypatch, config, kg) from mempalace.mcp_server import tool_check_duplicate # Exact match text from seeded_collection should be flagged @@ -263,7 +262,7 @@ class TestWriteTools: class TestKGTools: def test_kg_add(self, monkeypatch, config, palace_path, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) + _patch_mcp_server(monkeypatch, config, kg) from mempalace.mcp_server import tool_kg_add result = tool_kg_add( @@ -275,14 +274,14 @@ class TestKGTools: assert result["success"] is True def test_kg_query(self, monkeypatch, config, palace_path, seeded_kg): - _patch_mcp_server(monkeypatch, config, palace_path, seeded_kg) + _patch_mcp_server(monkeypatch, config, seeded_kg) from mempalace.mcp_server import tool_kg_query result = tool_kg_query(entity="Max") assert result["count"] > 0 def test_kg_invalidate(self, monkeypatch, config, palace_path, seeded_kg): - _patch_mcp_server(monkeypatch, config, palace_path, seeded_kg) + _patch_mcp_server(monkeypatch, config, seeded_kg) from mempalace.mcp_server import tool_kg_invalidate result = tool_kg_invalidate( @@ -294,14 +293,14 @@ class TestKGTools: assert result["success"] is True def test_kg_timeline(self, monkeypatch, config, palace_path, seeded_kg): - _patch_mcp_server(monkeypatch, config, palace_path, seeded_kg) + _patch_mcp_server(monkeypatch, config, seeded_kg) from mempalace.mcp_server import tool_kg_timeline result = tool_kg_timeline(entity="Alice") assert result["count"] > 0 def test_kg_stats(self, monkeypatch, config, palace_path, seeded_kg): - _patch_mcp_server(monkeypatch, config, palace_path, seeded_kg) + _patch_mcp_server(monkeypatch, config, seeded_kg) from mempalace.mcp_server import tool_kg_stats result = tool_kg_stats() @@ -313,8 +312,8 @@ class TestKGTools: class TestDiaryTools: def test_diary_write_and_read(self, monkeypatch, config, palace_path, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) - _get_collection(palace_path, create=True) + _patch_mcp_server(monkeypatch, config, kg) + _client, _col = _get_collection(palace_path, create=True); del _client from mempalace.mcp_server import tool_diary_write, tool_diary_read w = tool_diary_write( @@ -331,8 +330,8 @@ class TestDiaryTools: assert "authentication" in r["entries"][0]["content"] def test_diary_read_empty(self, monkeypatch, config, palace_path, kg): - _patch_mcp_server(monkeypatch, config, palace_path, kg) - _get_collection(palace_path, create=True) + _patch_mcp_server(monkeypatch, config, kg) + _client, _col = _get_collection(palace_path, create=True); del _client from mempalace.mcp_server import tool_diary_read r = tool_diary_read(agent_name="Nobody") From af42a850f6483eaa15487601213b3465125aa232 Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:46:34 -0300 Subject: [PATCH 35/38] fix: split semicolon statements onto two lines for ruff E702 --- tests/test_mcp_server.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index aff9df3..24258a9 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -82,7 +82,8 @@ class TestHandleRequest: from mempalace.mcp_server import handle_request # Create a collection so status works - _client, _col = _get_collection(palace_path, create=True); del _client + _client, _col = _get_collection(palace_path, create=True) + del _client resp = handle_request( { @@ -102,7 +103,8 @@ class TestHandleRequest: class TestReadTools: def test_status_empty_palace(self, monkeypatch, config, palace_path, kg): _patch_mcp_server(monkeypatch, config, kg) - _client, _col = _get_collection(palace_path, create=True); del _client + _client, _col = _get_collection(palace_path, create=True) + del _client from mempalace.mcp_server import tool_status result = tool_status() @@ -196,7 +198,8 @@ class TestSearchTool: class TestWriteTools: def test_add_drawer(self, monkeypatch, config, palace_path, kg): _patch_mcp_server(monkeypatch, config, kg) - _client, _col = _get_collection(palace_path, create=True); del _client + _client, _col = _get_collection(palace_path, create=True) + del _client from mempalace.mcp_server import tool_add_drawer result = tool_add_drawer( @@ -211,7 +214,8 @@ class TestWriteTools: def test_add_drawer_duplicate_detection(self, monkeypatch, config, palace_path, kg): _patch_mcp_server(monkeypatch, config, kg) - _client, _col = _get_collection(palace_path, create=True); del _client + _client, _col = _get_collection(palace_path, create=True) + del _client from mempalace.mcp_server import tool_add_drawer content = "This is a unique test memory about Rust ownership and borrowing." @@ -313,7 +317,8 @@ class TestKGTools: class TestDiaryTools: def test_diary_write_and_read(self, monkeypatch, config, palace_path, kg): _patch_mcp_server(monkeypatch, config, kg) - _client, _col = _get_collection(palace_path, create=True); del _client + _client, _col = _get_collection(palace_path, create=True) + del _client from mempalace.mcp_server import tool_diary_write, tool_diary_read w = tool_diary_write( @@ -331,7 +336,8 @@ class TestDiaryTools: def test_diary_read_empty(self, monkeypatch, config, palace_path, kg): _patch_mcp_server(monkeypatch, config, kg) - _client, _col = _get_collection(palace_path, create=True); del _client + _client, _col = _get_collection(palace_path, create=True) + del _client from mempalace.mcp_server import tool_diary_read r = tool_diary_read(agent_name="Nobody") From a0bcd0c836fc326e5904f22b40feb2feefd90dc5 Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:12:12 -0300 Subject: [PATCH 36/38] fix: ruff format test_hooks_cli.py and test_knowledge_graph.py --- tests/test_hooks_cli.py | 65 +++++++++++++++++++++-------------- tests/test_knowledge_graph.py | 2 +- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/tests/test_hooks_cli.py b/tests/test_hooks_cli.py index 8eeffed..d6951e2 100644 --- a/tests/test_hooks_cli.py +++ b/tests/test_hooks_cli.py @@ -42,29 +42,43 @@ def _write_transcript(path: Path, entries: list[dict]): def test_count_human_messages_basic(tmp_path): transcript = tmp_path / "t.jsonl" - _write_transcript(transcript, [ - {"message": {"role": "user", "content": "hello"}}, - {"message": {"role": "assistant", "content": "hi"}}, - {"message": {"role": "user", "content": "bye"}}, - ]) + _write_transcript( + transcript, + [ + {"message": {"role": "user", "content": "hello"}}, + {"message": {"role": "assistant", "content": "hi"}}, + {"message": {"role": "user", "content": "bye"}}, + ], + ) assert _count_human_messages(str(transcript)) == 2 def test_count_skips_command_messages(tmp_path): transcript = tmp_path / "t.jsonl" - _write_transcript(transcript, [ - {"message": {"role": "user", "content": "status"}}, - {"message": {"role": "user", "content": "real question"}}, - ]) + _write_transcript( + transcript, + [ + {"message": {"role": "user", "content": "status"}}, + {"message": {"role": "user", "content": "real question"}}, + ], + ) assert _count_human_messages(str(transcript)) == 1 def test_count_handles_list_content(tmp_path): transcript = tmp_path / "t.jsonl" - _write_transcript(transcript, [ - {"message": {"role": "user", "content": [{"type": "text", "text": "hello"}]}}, - {"message": {"role": "user", "content": [{"type": "text", "text": "x"}]}}, - ]) + _write_transcript( + transcript, + [ + {"message": {"role": "user", "content": [{"type": "text", "text": "hello"}]}}, + { + "message": { + "role": "user", + "content": [{"type": "text", "text": "x"}], + } + }, + ], + ) assert _count_human_messages(str(transcript)) == 1 @@ -90,6 +104,7 @@ def test_count_malformed_json_lines(tmp_path): def _capture_hook_output(hook_fn, data, harness="claude-code", state_dir=None): """Run a hook and capture its JSON stdout output.""" import io + buf = io.StringIO() patches = [patch("mempalace.hooks_cli._output", side_effect=lambda d: buf.write(json.dumps(d)))] if state_dir: @@ -123,10 +138,10 @@ def test_stop_hook_passthrough_when_active_string(tmp_path): def test_stop_hook_passthrough_below_interval(tmp_path): transcript = tmp_path / "t.jsonl" - _write_transcript(transcript, [ - {"message": {"role": "user", "content": f"msg {i}"}} - for i in range(SAVE_INTERVAL - 1) - ]) + _write_transcript( + transcript, + [{"message": {"role": "user", "content": f"msg {i}"}} for i in range(SAVE_INTERVAL - 1)], + ) result = _capture_hook_output( hook_stop, {"session_id": "test", "stop_hook_active": False, "transcript_path": str(transcript)}, @@ -137,10 +152,10 @@ def test_stop_hook_passthrough_below_interval(tmp_path): def test_stop_hook_blocks_at_interval(tmp_path): transcript = tmp_path / "t.jsonl" - _write_transcript(transcript, [ - {"message": {"role": "user", "content": f"msg {i}"}} - for i in range(SAVE_INTERVAL) - ]) + _write_transcript( + transcript, + [{"message": {"role": "user", "content": f"msg {i}"}} for i in range(SAVE_INTERVAL)], + ) result = _capture_hook_output( hook_stop, {"session_id": "test", "stop_hook_active": False, "transcript_path": str(transcript)}, @@ -152,10 +167,10 @@ def test_stop_hook_blocks_at_interval(tmp_path): def test_stop_hook_tracks_save_point(tmp_path): transcript = tmp_path / "t.jsonl" - _write_transcript(transcript, [ - {"message": {"role": "user", "content": f"msg {i}"}} - for i in range(SAVE_INTERVAL) - ]) + _write_transcript( + transcript, + [{"message": {"role": "user", "content": f"msg {i}"}} for i in range(SAVE_INTERVAL)], + ) data = {"session_id": "test", "stop_hook_active": False, "transcript_path": str(transcript)} # First call blocks diff --git a/tests/test_knowledge_graph.py b/tests/test_knowledge_graph.py index 535eace..d7d9838 100644 --- a/tests/test_knowledge_graph.py +++ b/tests/test_knowledge_graph.py @@ -6,7 +6,6 @@ timeline, stats, and edge cases (duplicate triples, ID collisions). """ - class TestEntityOperations: def test_add_entity(self, kg): eid = kg.add_entity("Alice", entity_type="person") @@ -125,6 +124,7 @@ class TestWALMode: conn.close() assert mode == "wal" + class TestStats: def test_stats_empty(self, kg): stats = kg.stats() From edf8f36099b3a6e3b9bb0b86678e4284d5e07078 Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:18:40 -0300 Subject: [PATCH 37/38] fix: use parse_known_args to allow importing mcp_server during pytest collection --- mempalace/mcp_server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mempalace/mcp_server.py b/mempalace/mcp_server.py index dcaff62..7d263a6 100644 --- a/mempalace/mcp_server.py +++ b/mempalace/mcp_server.py @@ -44,7 +44,8 @@ def _parse_args(): metavar="PATH", help="Path to the palace directory (overrides config file and env var)", ) - return parser.parse_args() + args, _ = parser.parse_known_args() + return args _args = _parse_args() From cef5994ea623b0874e0db15f245fa994f70e3d36 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 18:58:39 +0000 Subject: [PATCH 38/38] chore: bump version to 3.0.12 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- mempalace/version.py | 2 +- pyproject.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 0794c95..97f4463 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "mempalace", "source": "./.claude-plugin", "description": "AI memory system — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, guided setup.", - "version": "3.0.11", + "version": "3.0.12", "author": { "name": "milla-jovovich" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 295b247..7d927d4 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.11", + "version": "3.0.12", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 480a21f..7d1a09e 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mempalace", - "version": "3.0.11", + "version": "3.0.12", "description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.", "author": { "name": "milla-jovovich" diff --git a/mempalace/version.py b/mempalace/version.py index 6bf70fc..b2a0d20 100644 --- a/mempalace/version.py +++ b/mempalace/version.py @@ -1,3 +1,3 @@ """Single source of truth for the MemPalace package version.""" -__version__ = "3.0.11" +__version__ = "3.0.12" diff --git a/pyproject.toml b/pyproject.toml index 00e5c93..c5ee2c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mempalace" -version = "3.0.11" +version = "3.0.12" description = "Give your AI a memory — mine projects and conversations into a searchable palace. No API key required." readme = "README.md" requires-python = ">=3.9"