diff --git a/memory/SKILL.md b/memory/SKILL.md new file mode 100644 index 0000000..ccdc16d --- /dev/null +++ b/memory/SKILL.md @@ -0,0 +1,130 @@ +--- +name: memory +description: > + This skill should be used when the user wants to store, retrieve, update, or delete + persistent memories with hierarchical categories. Supports MCP JSON-RPC interface + and CLI management with multi-format export. +--- + +# Memory Manager + +Hierarchical memory storage with categories, MCP support, and multi-format export. + +## Architecture + +``` +memory/ +├── SKILL.md +├── memories/ # Root storage +│ ├── index.md # Root index +│ ├── work/ # Category: work +│ │ ├── project-a/ # Sub-category +│ │ │ └── .md # Memories +│ │ └── .md +│ ├── personal/ # Category: personal +│ └── preferences/ # Category: preferences +└── scripts/ + └── store.py # Main storage (CLI + MCP) +``` + +## When to Use + +- User asks to "remember", "save", or "store" something +- User wants to "recall" or "retrieve" stored information +- User wants to organize memories into categories +- User asks to "forget" or "delete" stored information +- Export memories in different formats + +## CLI Usage + +```bash +# Add memory to category +python store.py add -t "API Key" -c "sk-xxx" --category work/secrets --tags secret api + +# Get memory +python store.py get abc12345 --category work/secrets + +# List all (recursive) +python store.py list + +# List specific category (no recursion) +python store.py list --category work --no-recursive + +# Search in category +python store.py search "api" --category work + +# Update memory +python store.py update abc12345 -c "new content" +python store.py update abc12345 --move new/category # Move to category + +# Delete memory +python store.py delete abc12345 + +# List category structure +python store.py ls / +python store.py ls work + +# Create/remove category +python store.py mkdir new/category +python store.py rmdir empty/category + +# Export +python store.py export --format json -o memories.json +python store.py export --format markdown -o memories.md +python store.py export --format csv -o memories.csv +python store.py export --category work --format json +``` + +## Category Structure + +- **Root (`/`)**: Default, memories without category +- **Nested (`work/projects/api`)**: Hierarchical categories using `/` separator +- **Case-sensitive**: `Work` ≠ `work` +- **Auto-create**: Parent categories created automatically + +## Memory File Format + +```markdown +--- +category: work/projects +created: 2024-01-01T00:00:00 +id: abc12345 +tags: [api, backend] +title: API Design +updated: 2024-01-01T00:00:00 +--- + +Memory content goes here. +``` + +## MCP JSON-RPC Interface + +Run as MCP server: `python store.py --mcp` + +### Methods + +| Method | Parameters | Description | +|--------|-----------|-------------| +| `add` | `title`, `content`, `category?`, `tags?` | Add memory | +| `get` | `id`, `category?` | Get memory | +| `list` | `category?`, `recursive?` | List memories | +| `search` | `query`, `category?` | Search memories | +| `update` | `id`, `category?`, `title?`, `content?`, `tags?`, `new_category?` | Update/Move | +| `delete` | `id`, `category?` | Delete memory | +| `ls` | `category?` | List category structure | +| `mkdir` | `category` | Create category | +| `rmdir` | `category` | Remove empty category | +| `export` | `format?`, `category?` | Export memories | + +## Export Formats + +- **JSON**: Array of memory objects +- **Markdown**: Human-readable with category sections +- **CSV**: Spreadsheet-compatible + +## Tips + +- Use categories to organize: `work/`, `personal/`, `preferences/` +- Add tags for cross-category search +- Use `ls /` to view full structure +- Export specific category: `--category work` diff --git a/memory/memories/index.md b/memory/memories/index.md new file mode 100644 index 0000000..810e487 --- /dev/null +++ b/memory/memories/index.md @@ -0,0 +1,4 @@ +# Memory Index + +| ID | Title | Tags | +|---|-------|------| diff --git a/memory/scripts/manager.py b/memory/scripts/manager.py new file mode 100644 index 0000000..bbec269 --- /dev/null +++ b/memory/scripts/manager.py @@ -0,0 +1,258 @@ +"""Memory Manager - CLI tool for managing persistent memories.""" + +import argparse +import json +import os +import re +import sys +from datetime import datetime +from pathlib import Path +from typing import Optional + +MEMORY_DIR = Path(__file__).parent.parent / "memories" +INDEX_FILE = MEMORY_DIR / "index.md" + + +def ensure_dirs(): + """Ensure memory directory exists.""" + MEMORY_DIR.mkdir(parents=True, exist_ok=True) + if not INDEX_FILE.exists(): + INDEX_FILE.write_text("# Memory Index\n\n", encoding="utf-8") + + +def generate_id(title: str) -> str: + """Generate a short ID from title.""" + import hashlib + return hashlib.md5(title.encode()).hexdigest()[:8] + + +def parse_frontmatter(content: str) -> tuple[dict, str]: + """Parse YAML frontmatter from markdown content.""" + pattern = r'^---\n(.*?)\n---\n?(.*)$' + match = re.match(pattern, content, re.DOTALL) + if match: + frontmatter_text = match.group(1) + body = match.group(2) + + fm = {} + for line in frontmatter_text.split('\n'): + if ':' in line: + key, value = line.split(':', 1) + key = key.strip() + value = value.strip().strip('[]"\'|') + if value.startswith('['): + fm[key] = [v.strip().strip('"\'') for v in value[1:-1].split(',')] + else: + fm[key] = value + return fm, body + return {}, content + + +def create_frontmatter(data: dict, body: str) -> str: + """Create markdown with frontmatter.""" + lines = ["---"] + for key, value in data.items(): + if isinstance(value, list): + lines.append(f"{key}: [{', '.join(value)}]") + else: + lines.append(f"{key}: {value}") + lines.append("---\n") + if body.strip(): + lines.append(body.strip()) + return "\n".join(lines) + + +def add_memory(title: str, content: str, tags: list[str] = None) -> str: + """Add a new memory.""" + ensure_dirs() + + memory_id = generate_id(title) + filepath = MEMORY_DIR / f"{memory_id}.md" + + if filepath.exists(): + print(f"Memory with title '{title}' already exists as {memory_id}", file=sys.stderr) + return memory_id + + now = datetime.now().isoformat() + data = { + "id": memory_id, + "title": title, + "created": now, + "updated": now, + "tags": tags or [] + } + + md_content = create_frontmatter(data, content) + filepath.write_text(md_content, encoding="utf-8") + + update_index(memory_id, title, tags or []) + + print(f"Memory added: {memory_id}") + return memory_id + + +def update_index(memory_id: str, title: str, tags: list[str]): + """Update the index file.""" + lines = ["# Memory Index\n", "| ID | Title | Tags |", "|---|-------|------|"] + + for md_file in sorted(MEMORY_DIR.glob("*.md")): + if md_file.name == "index.md": + continue + fm, _ = parse_frontmatter(md_file.read_text(encoding="utf-8")) + tags_str = ", ".join(fm.get("tags", [])) + lines.append(f"| {fm.get('id', '')} | {fm.get('title', '')} | {tags_str} |") + + INDEX_FILE.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def get_memory(memory_id: str): + """Get a specific memory.""" + filepath = MEMORY_DIR / f"{memory_id}.md" + if not filepath.exists(): + print(f"Memory {memory_id} not found", file=sys.stderr) + return None + + content = filepath.read_text(encoding="utf-8") + fm, body = parse_frontmatter(content) + + print(f"# {fm.get('title', 'Untitled')}") + print(f"ID: {memory_id}") + print(f"Created: {fm.get('created', 'N/A')}") + print(f"Updated: {fm.get('updated', 'N/A')}") + print(f"Tags: {', '.join(fm.get('tags', []))}") + print(f"\n---\n{body}") + return fm + + +def list_memories(): + """List all memories.""" + ensure_dirs() + + memories = [] + for md_file in MEMORY_DIR.glob("*.md"): + if md_file.name == "index.md": + continue + fm, _ = parse_frontmatter(md_file.read_text(encoding="utf-8")) + memories.append(fm) + + if not memories: + print("No memories found.") + return + + print(f"{'ID':<10} {'Title':<30} {'Tags':<20} {'Updated'}") + print("-" * 80) + for m in sorted(memories, key=lambda x: x.get('updated', ''), reverse=True): + tags = ", ".join(m.get('tags', [])[:2]) + print(f"{m.get('id', ''):<10} {m.get('title', ''):<30} {tags:<20} {m.get('updated', '')[:10]}") + + +def search_memories(query: str): + """Search memories by keyword.""" + ensure_dirs() + + results = [] + for md_file in MEMORY_DIR.glob("*.md"): + if md_file.name == "index.md": + continue + content = md_file.read_text(encoding="utf-8").lower() + if query.lower() in content: + fm, body = parse_frontmatter(md_file.read_text(encoding="utf-8")) + results.append(fm) + + if not results: + print(f"No memories found matching '{query}'") + return + + print(f"Found {len(results)} result(s):\n") + for m in results: + print(f"- [{m.get('id')}] {m.get('title')} ({', '.join(m.get('tags', []))})") + + +def update_memory(memory_id: str, title: Optional[str] = None, content: Optional[str] = None, tags: list[str] = None): + """Update an existing memory.""" + filepath = MEMORY_DIR / f"{memory_id}.md" + if not filepath.exists(): + print(f"Memory {memory_id} not found", file=sys.stderr) + return + + fm, body = parse_frontmatter(filepath.read_text(encoding="utf-8")) + + if title: + fm['title'] = title + if content: + body = content + if tags is not None: + fm['tags'] = tags + + fm['updated'] = datetime.now().isoformat() + + filepath.write_text(create_frontmatter(fm, body), encoding="utf-8") + update_index(memory_id, fm.get('title', ''), fm.get('tags', [])) + + print(f"Memory updated: {memory_id}") + + +def delete_memory(memory_id: str): + """Delete a memory.""" + filepath = MEMORY_DIR / f"{memory_id}.md" + if not filepath.exists(): + print(f"Memory {memory_id} not found", file=sys.stderr) + return + + filepath.unlink() + update_index("", "", []) + print(f"Memory deleted: {memory_id}") + + +def main(): + parser = argparse.ArgumentParser(description="Memory Manager CLI") + subparsers = parser.add_subparsers(dest="command", help="Commands") + + # Add command + add_parser = subparsers.add_parser("add", help="Add a new memory") + add_parser.add_argument("--title", "-t", required=True, help="Memory title") + add_parser.add_argument("--content", "-c", required=True, help="Memory content") + add_parser.add_argument("--tags", nargs="*", default=[], help="Tags") + + # Get command + get_parser = subparsers.add_parser("get", help="Get a memory") + get_parser.add_argument("id", help="Memory ID") + + # List command + subparsers.add_parser("list", help="List all memories") + + # Search command + search_parser = subparsers.add_parser("search", help="Search memories") + search_parser.add_argument("query", help="Search query") + + # Update command + update_parser = subparsers.add_parser("update", help="Update a memory") + update_parser.add_argument("id", help="Memory ID") + update_parser.add_argument("--title", "-t", help="New title") + update_parser.add_argument("--content", "-c", help="New content") + update_parser.add_argument("--tags", nargs="*", help="New tags") + + # Delete command + delete_parser = subparsers.add_parser("delete", help="Delete a memory") + delete_parser.add_argument("id", help="Memory ID") + + args = parser.parse_args() + + if args.command == "add": + add_memory(args.title, args.content, args.tags) + elif args.command == "get": + get_memory(args.id) + elif args.command == "list": + list_memories() + elif args.command == "search": + search_memories(args.query) + elif args.command == "update": + update_memory(args.id, args.title, args.content, args.tags) + elif args.command == "delete": + delete_memory(args.id) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/memory/scripts/store.py b/memory/scripts/store.py new file mode 100644 index 0000000..44da64f --- /dev/null +++ b/memory/scripts/store.py @@ -0,0 +1,483 @@ +"""Memory Store - Hierarchical file-based memory storage with MCP support.""" + +import argparse +import json +import os +import re +import sys +import hashlib +from datetime import datetime +from pathlib import Path +from typing import Optional, List + +# === Hierarchical Memory Store === + +class MemoryStore: + """Hierarchical file-based memory storage with categories.""" + + def __init__(self, memory_dir: Path): + self.memory_dir = Path(memory_dir) + self.memory_dir.mkdir(parents=True, exist_ok=True) + + def _generate_id(self, title: str) -> str: + return hashlib.md5(title.encode()).hexdigest()[:8] + + def _parse_frontmatter(self, content: str) -> tuple[dict, str]: + pattern = r'^---\n(.*?)\n---\n?(.*)$' + match = re.match(pattern, content, re.DOTALL) + if match: + fm_text = match.group(1) + body = match.group(2) + fm = {} + for line in fm_text.split('\n'): + if ':' in line: + key, value = line.split(':', 1) + key = key.strip() + value = value.strip().strip('[]"\'') + if value.startswith('['): + fm[key] = [v.strip().strip('"\'') for v in value[1:-1].split(',') if v.strip()] + else: + fm[key] = value + return fm, body + return {}, content + + def _create_frontmatter(self, data: dict, body: str) -> str: + lines = ["---"] + for key, value in sorted(data.items()): + if isinstance(value, list): + lines.append(f"{key}: [{', '.join(value)}]") + else: + lines.append(f"{key}: {value}") + lines.append("---\n") + if body.strip(): + lines.append(body.strip()) + return "\n".join(lines) + + def _get_category_path(self, category: str = None) -> Path: + if not category or category == "/": + return self.memory_dir + path = self.memory_dir / category.lstrip("/") + path.mkdir(parents=True, exist_ok=True) + return path + + def _get_memory_path(self, category: str, memory_id: str) -> Path: + return self._get_category_path(category) / f"{memory_id}.md" + + def _relpath(self, path: Path) -> str: + """Get relative path from memory_dir.""" + try: + return str(path.relative_to(self.memory_dir)).replace("\\", "/") + except ValueError: + return str(path) + + def add(self, title: str, content: str, category: str = None, tags: List[str] = None, id: str = None) -> str: + """Add memory to category.""" + memory_id = id or self._generate_id(title) + filepath = self._get_memory_path(category, memory_id) + + now = datetime.now().isoformat() + data = { + "id": memory_id, + "title": title, + "created": now, + "updated": now, + "tags": tags or [], + "category": category or "/" + } + + filepath.write_text(self._create_frontmatter(data, content), encoding="utf-8") + return memory_id + + def get(self, memory_id: str, category: str = None) -> Optional[dict]: + """Get memory by ID, optionally in category.""" + if category: + filepath = self._get_memory_path(category, memory_id) + if filepath.exists(): + return self._read_memory(filepath) + return None + + # Search all categories + for md_file in self.memory_dir.rglob("*.md"): + fm, _ = self._parse_frontmatter(md_file.read_text(encoding="utf-8")) + if fm.get("id") == memory_id: + result = fm + result["content"] = self._read_memory(md_file)["content"] + return result + return None + + def _read_memory(self, filepath: Path) -> dict: + fm, body = self._parse_frontmatter(filepath.read_text(encoding="utf-8")) + fm["content"] = body.strip() + fm["category"] = self._relpath(filepath.parent) + return fm + + def list(self, category: str = None, recursive: bool = True) -> List[dict]: + """List memories in category.""" + memories = [] + cat_path = self._get_category_path(category) + + pattern = "*.md" if recursive else "*.md" + for md_file in cat_path.glob(pattern): + if md_file.name.startswith("_"): + continue + fm, _ = self._parse_frontmatter(md_file.read_text(encoding="utf-8")) + fm["category"] = self._relpath(md_file.parent) + memories.append(fm) + + return sorted(memories, key=lambda x: (x.get('category', ''), x.get('updated', '')), reverse=True) + + def search(self, query: str, category: str = None) -> List[dict]: + """Search memories.""" + results = [] + query_lower = query.lower() + base_path = self._get_category_path(category) + + for md_file in base_path.rglob("*.md"): + if md_file.name.startswith("_"): + continue + content = md_file.read_text(encoding="utf-8").lower() + if query_lower in content: + fm, body = self._parse_frontmatter(md_file.read_text(encoding="utf-8")) + fm["content"] = body.strip() + fm["category"] = self._relpath(md_file.parent) + results.append(fm) + + return results + + def update(self, memory_id: str, category: str = None, title: str = None, + content: str = None, tags: List[str] = None, new_category: str = None) -> bool: + """Update memory, optionally move to new category.""" + old_path = self._get_memory_path(category, memory_id) + if not old_path.exists(): + # Try search across all + for md_file in self.memory_dir.rglob("*.md"): + fm, _ = self._parse_frontmatter(md_file.read_text(encoding="utf-8")) + if fm.get("id") == memory_id: + old_path = md_file + category = self._relpath(md_file.parent) + break + else: + return False + + fm, body = self._parse_frontmatter(old_path.read_text(encoding="utf-8")) + + if title is not None: + fm['title'] = title + if content is not None: + body = content + if tags is not None: + fm['tags'] = tags + + target_cat = new_category if new_category is not None else category + fm['updated'] = datetime.now().isoformat() + fm['category'] = target_cat or "/" + + new_path = self._get_memory_path(target_cat, memory_id) + + if new_path != old_path: + old_path.unlink() + + new_path.write_text(self._create_frontmatter(fm, body), encoding="utf-8") + return True + + def delete(self, memory_id: str, category: str = None) -> bool: + """Delete memory.""" + filepath = self._get_memory_path(category, memory_id) + if not filepath.exists(): + # Search all + for md_file in self.memory_dir.rglob("*.md"): + fm, _ = self._parse_frontmatter(md_file.read_text(encoding="utf-8")) + if fm.get("id") == memory_id: + filepath = md_file + break + else: + return False + + filepath.unlink() + + # Clean empty category dirs + cat_dir = filepath.parent + if cat_dir != self.memory_dir and not any(cat_dir.iterdir()): + cat_dir.rmdir() + + return True + + def ls(self, category: str = None) -> dict: + """List category structure (like ls -la).""" + cat_path = self._get_category_path(category) + result = { + "path": self._relpath(cat_path), + "categories": [], + "memories": [] + } + + for item in sorted(cat_path.iterdir()): + if item.name.startswith("_"): + continue + if item.is_dir(): + result["categories"].append(item.name) + else: + fm, _ = self._parse_frontmatter(item.read_text(encoding="utf-8")) + result["memories"].append({ + "id": fm.get("id"), + "title": fm.get("title"), + "tags": fm.get("tags", []) + }) + + return result + + def mkdir(self, category: str) -> bool: + """Create category.""" + cat_path = self._get_category_path(category) + if cat_path.exists(): + return False + cat_path.mkdir(parents=True) + return True + + def rmdir(self, category: str) -> bool: + """Remove empty category.""" + cat_path = self._get_category_path(category) + if cat_path == self.memory_dir or not cat_path.exists(): + return False + + # Check empty + items = list(cat_path.iterdir()) + if any(not i.name.startswith("_") for i in items): + return False + + for item in items: + if item.is_file(): + item.unlink() + elif item.is_dir(): + import shutil + shutil.rmtree(item) + cat_path.rmdir() + return True + + def export(self, format: str = "json", category: str = None) -> str: + """Export memories.""" + memories = self.list(category=category) + + if format == "json": + return json.dumps(memories, indent=2, ensure_ascii=False) + elif format == "markdown": + lines = ["# Memory Export\n"] + current_cat = None + for m in memories: + cat = m.get("category", "/") + if cat != current_cat: + lines.append(f"\n## {cat}/\n") + current_cat = cat + lines.append(f"### {m.get('title', 'Untitled')}\n") + lines.append(f"**ID:** `{m.get('id', '')}` **Tags:** {', '.join(m.get('tags', []))}\n\n") + return "".join(lines) + elif format == "csv": + lines = ["id,title,tags,category,created,updated,content\n"] + for m in memories: + tags = "|".join(m.get('tags', [])) + content = (m.get('content', '') or '').replace('"', '""').replace('\n', ' ') + lines.append(f'"{m.get("id", "")}","{m.get("title", "")}","{tags}","{m.get("category", "")}","{m.get("created", "")}","{m.get("updated", "")}","{content}"\n') + return "".join(lines) + return json.dumps(memories) + + +# === CLI Interface === + +def cli(): + parser = argparse.ArgumentParser(description="Memory Store CLI") + parser.add_argument("--path", type=Path, default=Path(__file__).parent.parent / "memories") + + subparsers = parser.add_subparsers(dest="command", help="Commands") + + # Add + add_p = subparsers.add_parser("add", help="Add memory") + add_p.add_argument("-t", "--title", required=True) + add_p.add_argument("-c", "--content", required=True) + add_p.add_argument("--tags", nargs="*", default=[]) + add_p.add_argument("--category", "-C", default="/") + + # Get + get_p = subparsers.add_parser("get", help="Get memory") + get_p.add_argument("id") + get_p.add_argument("--category", "-C") + + # List + list_p = subparsers.add_parser("list", help="List memories") + list_p.add_argument("--category", "-C") + list_p.add_argument("--no-recursive", action="store_true") + + # Search + search_p = subparsers.add_parser("search", help="Search") + search_p.add_argument("query") + search_p.add_argument("--category", "-C") + + # Update + update_p = subparsers.add_parser("update", help="Update") + update_p.add_argument("id") + update_p.add_argument("--category", "-C") + update_p.add_argument("-t", "--title") + update_p.add_argument("-c", "--content") + update_p.add_argument("--tags", nargs="*") + update_p.add_argument("--move", "-M", dest="new_category") + + # Delete + delete_p = subparsers.add_parser("delete", help="Delete") + delete_p.add_argument("id") + delete_p.add_argument("--category", "-C") + + # ls (tree) + ls_p = subparsers.add_parser("ls", help="List structure") + ls_p.add_argument("--category", "-C", default="/") + + # mkdir + mkdir_p = subparsers.add_parser("mkdir", help="Create category") + mkdir_p.add_argument("category") + + # rmdir + rmdir_p = subparsers.add_parser("rmdir", help="Remove empty category") + rmdir_p.add_argument("category") + + # Export + export_p = subparsers.add_parser("export", help="Export") + export_p.add_argument("--format", choices=["json", "markdown", "csv"], default="json") + export_p.add_argument("-o", "--output", type=Path) + export_p.add_argument("--category", "-C") + + args = parser.parse_args() + store = MemoryStore(args.path) + + if args.command == "add": + mid = store.add(args.title, args.content, args.category, args.tags) + print(f"Added: {mid} -> {args.category}") + + elif args.command == "get": + m = store.get(args.id, args.category) + if m: + print(f"# {m['title']}\n") + print(f"ID: {m['id']} Category: {m.get('category', '/')} Tags: {', '.join(m.get('tags', []))}") + print(f"Created: {m['created']} Updated: {m['updated']}\n") + print("---") + print(m['content']) + else: + print(f"Not found: {args.id}", file=sys.stderr) + + elif args.command == "list": + for m in store.list(args.category, not args.no_recursive): + cat = m.get('category', '/') + tags = ", ".join(m.get('tags', [])[:2]) + print(f"{m['id']:<10} [{cat:<15}] {m['title']:<30} [{tags}]") + + elif args.command == "search": + for m in store.search(args.query, args.category): + print(f"- [{m.get('category', '/')}] {m['id']} {m['title']} ({', '.join(m.get('tags', []))})") + + elif args.command == "update": + ok = store.update(args.id, args.category, args.title, args.content, args.tags, args.new_category) + print("Updated" if ok else "Not found") + + elif args.command == "delete": + ok = store.delete(args.id, args.category) + print("Deleted" if ok else "Not found") + + elif args.command == "ls": + result = store.ls(args.category) + print(f"📁 {result['path']}/") + if result['categories']: + for c in result['categories']: + print(f" 📂 {c}/") + for m in result['memories']: + print(f" 📄 {m['id']} - {m['title']}") + + elif args.command == "mkdir": + ok = store.mkdir(args.category) + print("Created" if ok else "Already exists") + + elif args.command == "rmdir": + ok = store.rmdir(args.category) + print("Removed" if ok else "Failed (not empty or not found)") + + elif args.command == "export": + output = store.export(args.format, args.category) + if args.output: + args.output.write_text(output, encoding="utf-8") + print(f"Exported to {args.output}") + else: + print(output) + + else: + parser.print_help() + + +# === MCP JSON-RPC Server === + +def mcp_server(): + """Run as MCP JSON-RPC server (stdio).""" + store = MemoryStore(Path(__file__).parent.parent / "memories") + + for line in sys.stdin: + try: + request = json.loads(line.strip()) + method = request.get("method", "") + params = request.get("params", {}) + req_id = request.get("id") + + result = None + error = None + + try: + if method == "add": + result = store.add( + params.get("title", ""), + params.get("content", ""), + params.get("category"), + params.get("tags", []) + ) + elif method == "get": + result = store.get(params.get("id", ""), params.get("category")) + elif method == "list": + result = store.list(params.get("category"), params.get("recursive", True)) + elif method == "search": + result = store.search(params.get("query", ""), params.get("category")) + elif method == "update": + result = store.update( + params.get("id", ""), + params.get("category"), + params.get("title"), + params.get("content"), + params.get("tags"), + params.get("new_category") + ) + elif method == "delete": + result = store.delete(params.get("id", ""), params.get("category")) + elif method == "ls": + result = store.ls(params.get("category")) + elif method == "mkdir": + result = store.mkdir(params.get("category", "")) + elif method == "rmdir": + result = store.rmdir(params.get("category", "")) + elif method == "export": + result = store.export(params.get("format", "json"), params.get("category")) + else: + error = {"code": -32601, "message": f"Unknown method: {method}"} + except Exception as e: + error = {"code": -32603, "message": str(e)} + + response = {"jsonrpc": "2.0", "id": req_id} + if error: + response["error"] = error + else: + response["result"] = result + + print(json.dumps(response), flush=True) + + except json.JSONDecodeError: + continue + except Exception: + continue + + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "--mcp": + mcp_server() + else: + cli()