From 48cfa26271ba663b028a9679c472cfbd03708381 Mon Sep 17 00:00:00 2001 From: emirbartu Date: Thu, 9 Apr 2026 00:40:21 +0300 Subject: [PATCH 1/4] fix(search): include drawer_id in search results Add 'id' field to search results containing the drawer ID. This enables the delete/update workflow after finding content via search. - Extract drawer IDs from results["ids"][0] (index-aligned with documents) - Include 'id' field in each hit dictionary - Add test to verify drawer_id is present in search results Fixes #224 --- mempalace/searcher.py | 4 +++- tests/test_searcher.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/mempalace/searcher.py b/mempalace/searcher.py index 163abd88c5..e8e2eab05b 100644 --- a/mempalace/searcher.py +++ b/mempalace/searcher.py @@ -129,14 +129,16 @@ def search_memories( except Exception as e: return {"error": f"Search error: {e}"} + ids = results["ids"][0] docs = results["documents"][0] metas = results["metadatas"][0] dists = results["distances"][0] hits = [] - for doc, meta, dist in zip(docs, metas, dists): + for drawer_id, doc, meta, dist in zip(ids, docs, metas, dists): hits.append( { + "id": drawer_id, "text": doc, "wing": meta.get("wing", "unknown"), "room": meta.get("room", "unknown"), diff --git a/tests/test_searcher.py b/tests/test_searcher.py index 1c2687d461..a711c1c293 100644 --- a/tests/test_searcher.py +++ b/tests/test_searcher.py @@ -43,3 +43,9 @@ def test_result_fields(self, palace_path, seeded_collection): assert "source_file" in hit assert "similarity" in hit assert isinstance(hit["similarity"], float) + + def test_result_includes_drawer_id(self, palace_path, seeded_collection): + result = search_memories("authentication", palace_path) + hit = result["results"][0] + assert "id" in hit + assert hit["id"].startswith("drawer_") From 27e42522455cc4ec25be609de718d0817cde0940 Mon Sep 17 00:00:00 2001 From: emirbartu Date: Thu, 9 Apr 2026 00:40:33 +0300 Subject: [PATCH 2/4] feat(mcp): add bulk delete tools for wings and rooms Implement bulk deletion capabilities via MCP tools: - mempdelete_wing: Delete all drawers in a wing using where filter - mempdelete_room: Delete specific room in a wing using \ filter Both tools: - Check existence before deletion - Return deleted_count in response - Log deletion for audit trail - Include comprehensive tests Fixes #237 Fixes #209 --- mempalace/mcp_server.py | 64 ++++++++++++++++++++++++++++++++++++++++ tests/test_mcp_server.py | 36 ++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/mempalace/mcp_server.py b/mempalace/mcp_server.py index 7d263a66c2..23beb95636 100644 --- a/mempalace/mcp_server.py +++ b/mempalace/mcp_server.py @@ -331,6 +331,47 @@ def tool_delete_drawer(drawer_id: str): return {"success": False, "error": str(e)} +def tool_delete_wing(wing: str): + """Delete all drawers in a wing. Irreversible.""" + col = _get_collection() + if not col: + return _no_palace() + + results = col.get(where={"wing": wing}, include=[]) + count = len(results["ids"]) + + if count == 0: + return {"success": False, "error": f"Wing not found or empty: {wing}"} + + try: + col.delete(where={"wing": wing}) + logger.info(f"Deleted wing: {wing} ({count} drawers)") + return {"success": True, "wing": wing, "deleted_count": count} + except Exception as e: + return {"success": False, "error": str(e)} + + +def tool_delete_room(wing: str, room: str): + """Delete all drawers in a room. Irreversible.""" + col = _get_collection() + if not col: + return _no_palace() + + where = {"$and": [{"wing": wing}, {"room": room}]} + results = col.get(where=where, include=[]) + count = len(results["ids"]) + + if count == 0: + return {"success": False, "error": f"Room not found or empty: {wing}/{room}"} + + try: + col.delete(where=where) + logger.info(f"Deleted room: {wing}/{room} ({count} drawers)") + return {"success": True, "wing": wing, "room": room, "deleted_count": count} + except Exception as e: + return {"success": False, "error": str(e)} + + # ==================== KNOWLEDGE GRAPH ==================== @@ -674,6 +715,29 @@ def tool_diary_read(agent_name: str, last_n: int = 10): }, "handler": tool_delete_drawer, }, + "mempalace_delete_wing": { + "description": "Delete all drawers in a wing. Irreversible.", + "input_schema": { + "type": "object", + "properties": { + "wing": {"type": "string", "description": "Wing to delete"}, + }, + "required": ["wing"], + }, + "handler": tool_delete_wing, + }, + "mempalace_delete_room": { + "description": "Delete all drawers in a room. Irreversible.", + "input_schema": { + "type": "object", + "properties": { + "wing": {"type": "string", "description": "Wing containing the room"}, + "room": {"type": "string", "description": "Room to delete"}, + }, + "required": ["wing", "room"], + }, + "handler": tool_delete_room, + }, "mempalace_diary_write": { "description": "Write to your personal agent diary in AAAK format. Your observations, thoughts, what you worked on, what matters. Each agent has their own diary with full history. Write in AAAK for compression — e.g. 'SESSION:2026-04-04|built.palace.graph+diary.tools|ALC.req:agent.diaries.in.aaak|★★★'. Use entity codes from the AAAK spec.", "input_schema": { diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index 24258a9159..f1a4c5d5f4 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -58,6 +58,8 @@ def test_tools_list(self): assert "mempalace_search" in names assert "mempalace_add_drawer" in names assert "mempalace_kg_add" in names + assert "mempalace_delete_wing" in names + assert "mempalace_delete_room" in names def test_unknown_tool(self): from mempalace.mcp_server import handle_request @@ -241,6 +243,40 @@ def test_delete_drawer_not_found(self, monkeypatch, config, palace_path, seeded_ result = tool_delete_drawer("nonexistent_drawer") assert result["success"] is False + def test_delete_wing(self, monkeypatch, config, palace_path, seeded_collection, kg): + _patch_mcp_server(monkeypatch, config, kg) + from mempalace.mcp_server import tool_delete_wing + + result = tool_delete_wing("project") + assert result["success"] is True + assert result["deleted_count"] == 3 + assert seeded_collection.count() == 1 + + def test_delete_room(self, monkeypatch, config, palace_path, seeded_collection, kg): + _patch_mcp_server(monkeypatch, config, kg) + from mempalace.mcp_server import tool_delete_room + + result = tool_delete_room("project", "backend") + assert result["success"] is True + assert result["deleted_count"] == 2 + assert seeded_collection.count() == 2 + + def test_delete_wing_not_found(self, monkeypatch, config, palace_path, seeded_collection, kg): + _patch_mcp_server(monkeypatch, config, kg) + from mempalace.mcp_server import tool_delete_wing + + result = tool_delete_wing("nonexistent") + assert result["success"] is False + assert "wing not found" in result["error"].lower() + + def test_delete_room_not_found(self, monkeypatch, config, palace_path, seeded_collection, kg): + _patch_mcp_server(monkeypatch, config, kg) + from mempalace.mcp_server import tool_delete_room + + result = tool_delete_room("project", "nonexistent") + assert result["success"] is False + assert "room not found" in result["error"].lower() + def test_check_duplicate(self, monkeypatch, config, palace_path, seeded_collection, kg): _patch_mcp_server(monkeypatch, config, kg) from mempalace.mcp_server import tool_check_duplicate From aaa51286e5cb11733446065ec3c1997378939fa3 Mon Sep 17 00:00:00 2001 From: emirbartu Date: Thu, 9 Apr 2026 00:40:45 +0300 Subject: [PATCH 3/4] feat(cli): add delete-wing and delete-room commands Add CLI commands for bulk deletion: - mempalace delete-wing : Delete entire wing with confirmation - mempalace delete-room : Delete specific room Features: - Shows drawer count before deletion - Interactive confirmation prompt - --force flag to skip confirmation - Proper error handling for non-existent wings/rooms Fixes #237 --- mempalace/cli.py | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/mempalace/cli.py b/mempalace/cli.py index 0a24abf64d..6cd5fe3328 100644 --- a/mempalace/cli.py +++ b/mempalace/cli.py @@ -361,6 +361,71 @@ def cmd_compress(args): print(" (dry run -- nothing stored)") +def cmd_delete_wing(args): + """Delete an entire wing.""" + import chromadb + + config = MempalaceConfig() + palace_path = os.path.expanduser(args.palace) if args.palace else config.palace_path + + try: + client = chromadb.PersistentClient(path=palace_path) + col = client.get_collection(config.collection_name) + + results = col.get(where={"wing": args.wing}, include=[]) + count = len(results["ids"]) + + if count == 0: + print(f"Wing not found: {args.wing}") + return 1 + + if not args.force: + confirm = input(f"Delete wing '{args.wing}' with {count} drawers? [y/N] ") + if confirm.lower() != "y": + print("Aborted") + return 0 + + col.delete(where={"wing": args.wing}) + print(f"Deleted wing '{args.wing}' ({count} drawers)") + return 0 + except Exception as e: + print(f"Error: {e}") + return 1 + + +def cmd_delete_room(args): + """Delete a room in a wing.""" + import chromadb + + config = MempalaceConfig() + palace_path = os.path.expanduser(args.palace) if args.palace else config.palace_path + + try: + client = chromadb.PersistentClient(path=palace_path) + col = client.get_collection(config.collection_name) + + where = {"$and": [{"wing": args.wing}, {"room": args.room}]} + results = col.get(where=where, include=[]) + count = len(results["ids"]) + + if count == 0: + print(f"Room not found: {args.wing}/{args.room}") + return 1 + + if not args.force: + confirm = input(f"Delete room '{args.wing}/{args.room}' with {count} drawers? [y/N] ") + if confirm.lower() != "y": + print("Aborted") + return 0 + + col.delete(where=where) + print(f"Deleted room '{args.wing}/{args.room}' ({count} drawers)") + return 0 + except Exception as e: + print(f"Error: {e}") + return 1 + + def main(): parser = argparse.ArgumentParser( description="MemPalace — Give your AI a memory. No API key required.", @@ -503,6 +568,19 @@ def main(): # status sub.add_parser("status", help="Show what's been filed") + # delete-wing + p_delete_wing = sub.add_parser("delete-wing", help="Delete an entire wing") + p_delete_wing.add_argument("wing", help="Wing name to delete") + p_delete_wing.add_argument("--force", action="store_true", help="Skip confirmation") + p_delete_wing.set_defaults(func=cmd_delete_wing) + + # delete-room + p_delete_room = sub.add_parser("delete-room", help="Delete a room in a wing") + p_delete_room.add_argument("wing", help="Wing name") + p_delete_room.add_argument("room", help="Room name to delete") + p_delete_room.add_argument("--force", action="store_true", help="Skip confirmation") + p_delete_room.set_defaults(func=cmd_delete_room) + args = parser.parse_args() if not args.command: @@ -535,6 +613,8 @@ def main(): "wake-up": cmd_wakeup, "repair": cmd_repair, "status": cmd_status, + "delete-wing": cmd_delete_wing, + "delete-room": cmd_delete_room, } dispatch[args.command](args) From 72207ecaea1505744cfe620e3a1a2b7a728d0f99 Mon Sep 17 00:00:00 2001 From: emirbartu Date: Thu, 9 Apr 2026 00:40:56 +0300 Subject: [PATCH 4/4] docs: update README with bulk delete features and search drawer_id Update documentation to reflect new features: - Update tool count from 19 to 21 - Add mempdelete_wing and mempdelete_room to MCP tools table - Add delete-wing and delete-room CLI commands - Note that search results now include 'id' field - Update plugin READMEs with new tool count --- .claude-plugin/README.md | 2 +- README.md | 16 ++++++++++++---- mempalace/README.md | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md index fd98952409..a291eceb79 100644 --- a/.claude-plugin/README.md +++ b/.claude-plugin/README.md @@ -50,7 +50,7 @@ Set the `MEMPAL_DIR` environment variable to a directory path to automatically r ## 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. +The plugin automatically configures a local MCP server with 21 tools for storing, searching, and managing memories. No manual MCP setup is required -- `/mempalace:init` handles everything. ## Full Documentation diff --git a/README.md b/README.md index a1a7ccb783..e1cfdbf9e9 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Restart Claude Code, then type `/skills` to verify "mempalace" appears. claude mcp add mempalace -- python -m mempalace.mcp_server ``` -Now your AI has 19 tools available through MCP. Ask it anything: +Now your AI has 21 tools available through MCP. Ask it anything: > *"What did we decide about auth last month?"* @@ -458,7 +458,7 @@ claude plugin install --scope user mempalace claude mcp add mempalace -- python -m mempalace.mcp_server ``` -### 19 Tools +### 21 Tools **Palace (read)** @@ -472,12 +472,16 @@ claude mcp add mempalace -- python -m mempalace.mcp_server | `mempalace_check_duplicate` | Check before filing | | `mempalace_get_aaak_spec` | AAAK dialect reference | +Search results now include an `id` field for each hit (the drawer ID). + **Palace (write)** | Tool | What | |------|------| | `mempalace_add_drawer` | File verbatim content | | `mempalace_delete_drawer` | Remove by ID | +| `mempalace_delete_wing` | Delete all drawers in a wing | +| `mempalace_delete_room` | Delete all drawers in a room | **Knowledge Graph** @@ -576,6 +580,10 @@ mempalace search "query" # search everything mempalace search "query" --wing myapp # within a wing mempalace search "query" --room auth-migration # within a room +# Delete +mempalace delete-wing # delete entire wing +mempalace delete-room # delete specific room + # Memory stack mempalace wake-up # load L0 + L1 context mempalace wake-up --wing driftwood # project-specific @@ -630,7 +638,7 @@ Plain text. Becomes Layer 0 — loaded every session. | `cli.py` | CLI entry point | | `config.py` | Configuration loading and defaults | | `normalize.py` | Converts 5 chat formats to standard transcript | -| `mcp_server.py` | MCP server — 19 tools, AAAK auto-teach, memory protocol | +| `mcp_server.py` | MCP server — 21 tools, AAAK auto-teach, memory protocol | | `miner.py` | Project file ingest | | `convo_miner.py` | Conversation ingest — chunks by exchange pair | | `searcher.py` | Semantic search via ChromaDB | @@ -654,7 +662,7 @@ mempalace/ ├── README.md ← you are here ├── mempalace/ ← core package (README) │ ├── cli.py ← CLI entry point -│ ├── mcp_server.py ← MCP server (19 tools) +│ ├── mcp_server.py ← MCP server (21 tools) │ ├── knowledge_graph.py ← temporal entity graph │ ├── palace_graph.py ← room navigation graph │ ├── dialect.py ← AAAK compression diff --git a/mempalace/README.md b/mempalace/README.md index fdbbb62066..c74eae3089 100644 --- a/mempalace/README.md +++ b/mempalace/README.md @@ -16,7 +16,7 @@ The Python package that powers MemPalace. All modules, all logic. | `dialect.py` | AAAK compression — entity codes, emotion markers, 30x lossless ratio | | `knowledge_graph.py` | Temporal entity-relationship graph — SQLite, time-filtered queries, fact invalidation | | `palace_graph.py` | Room-based navigation graph — BFS traversal, tunnel detection across wings | -| `mcp_server.py` | MCP server — 19 tools, AAAK auto-teach, Palace Protocol, agent diary | +| `mcp_server.py` | MCP server — 21 tools, AAAK auto-teach, Palace Protocol, agent diary | | `onboarding.py` | Guided first-run setup — asks about people/projects, generates AAAK bootstrap + wing config | | `entity_registry.py` | Entity code registry — maps names to AAAK codes, handles ambiguous names | | `entity_detector.py` | Auto-detect people and projects from file content |