Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?"*

Expand Down Expand Up @@ -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)**

Expand All @@ -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**

Expand Down Expand Up @@ -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 <wing> # delete entire wing
mempalace delete-room <wing> <room> # delete specific room

# Memory stack
mempalace wake-up # load L0 + L1 context
mempalace wake-up --wing driftwood # project-specific
Expand Down Expand Up @@ -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 |
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion mempalace/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
80 changes: 80 additions & 0 deletions mempalace/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down
64 changes: 64 additions & 0 deletions mempalace/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ====================


Expand Down Expand Up @@ -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": {
Expand Down
4 changes: 3 additions & 1 deletion mempalace/searcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
36 changes: 36 additions & 0 deletions tests/test_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions tests/test_searcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_")