diff --git a/src/memory_manager/cli/main.py b/src/memory_manager/cli/main.py new file mode 100644 index 0000000..ae6a153 --- /dev/null +++ b/src/memory_manager/cli/main.py @@ -0,0 +1,245 @@ +"""CLI entry point for memory manager.""" +import os +import sys +from typing import Optional + +import click +import uvicorn + +from memory_manager.core.services import MemoryService, CommitService +from memory_manager.db.repository import MemoryRepository +from memory_manager.api.app import app + + +db_path = os.getenv("MEMORY_DB_PATH", ".memory/codebase_memory.db") +os.makedirs(os.path.dirname(db_path), exist_ok=True) + +repository = MemoryRepository(db_path) +memory_service = MemoryService(repository) +commit_service = CommitService(repository) + + +@click.group() +@click.version_option(version="0.1.0") +def cli(): + """Agentic Codebase Memory Manager - A shared memory layer for AI coding agents.""" + pass + + +@cli.command() +@click.option("--title", required=True, help="Entry title") +@click.option("--content", required=True, help="Entry content") +@click.option("--category", required=True, type=click.Choice(["decision", "feature", "refactoring", "architecture", "bug", "note"]), help="Entry category") +@click.option("--tags", default="", help="Comma-separated tags") +@click.option("--agent-id", default=lambda: os.getenv("AGENT_ID", "unknown"), help="Agent identifier") +def add(title: str, content: str, category: str, tags: str, agent_id: str): + """Add a new memory entry.""" + import asyncio + + async def _add(): + tag_list = [t.strip() for t in tags.split(",") if t.strip()] + entry = await memory_service.create_entry( + title=title, + content=content, + category=category, + tags=tag_list, + agent_id=agent_id, + ) + click.echo(f"Created entry {entry.id}: {entry.title}") + + asyncio.run(_add()) + + +@cli.command() +@click.option("--category", help="Filter by category") +@click.option("--agent-id", help="Filter by agent") +@click.option("--limit", default=100, help="Maximum entries to return") +def list(category: Optional[str], agent_id: Optional[str], limit: int): + """List memory entries.""" + import asyncio + + async def _list(): + entries = await memory_service.list_entries( + category=category, + agent_id=agent_id, + limit=limit, + ) + if not entries: + click.echo("No entries found.") + return + for entry in entries: + click.echo(f"[{entry.id}] {entry.category.value}: {entry.title}") + click.echo(f" {entry.content[:100]}...") + click.echo(f" Tags: {', '.join(entry.tags or [])}") + click.echo() + + asyncio.run(_list()) + + +@cli.command() +@click.argument("query") +@click.option("--category", help="Filter by category") +def search(query: str, category: Optional[str]): + """Search memory entries.""" + import asyncio + + async def _search(): + entries = await memory_service.search_entries( + query=query, + category=category, + ) + if not entries: + click.echo(f"No entries found matching '{query}'.") + return + click.echo(f"Found {len(entries)} entries:") + for entry in entries: + click.echo(f"[{entry.id}] {entry.category.value}: {entry.title}") + + asyncio.run(_search()) + + +@cli.command() +@click.argument("entry_id", type=int) +def get(entry_id: int): + """Get a specific entry.""" + import asyncio + + async def _get(): + entry = await memory_service.get_entry(entry_id) + if not entry: + click.echo(f"Entry {entry_id} not found.") + return + click.echo(f"Title: {entry.title}") + click.echo(f"Category: {entry.category.value}") + click.echo(f"Content: {entry.content}") + click.echo(f"Tags: {', '.join(entry.tags or [])}") + click.echo(f"Agent: {entry.agent_id}") + click.echo(f"Created: {entry.created_at}") + click.echo(f"Updated: {entry.updated_at}") + + asyncio.run(_get()) + + +@cli.command() +@click.argument("entry_id", type=int) +@click.option("--title", help="New title") +@click.option("--content", help="New content") +@click.option("--category", type=click.Choice(["decision", "feature", "refactoring", "architecture", "bug", "note"]), help="New category") +@click.option("--tags", help="Comma-separated tags") +def update(entry_id: int, title: Optional[str], content: Optional[str], category: Optional[str], tags: Optional[str]): + """Update a memory entry.""" + import asyncio + + async def _update(): + tag_list = [t.strip() for t in tags.split(",")] if tags else None + updated = await memory_service.update_entry( + entry_id=entry_id, + title=title, + content=content, + category=category, + tags=tag_list, + ) + if not updated: + click.echo(f"Entry {entry_id} not found.") + return + click.echo(f"Updated entry {entry_id}: {updated.title}") + + asyncio.run(_update()) + + +@cli.command() +@click.argument("entry_id", type=int) +def delete(entry_id: int): + """Delete a memory entry.""" + import asyncio + + async def _delete(): + deleted = await memory_service.delete_entry(entry_id) + if not deleted: + click.echo(f"Entry {entry_id} not found.") + return + click.echo(f"Deleted entry {entry_id}.") + + asyncio.run(_delete()) + + +@cli.command() +@click.option("--message", required=True, help="Commit message") +@click.option("--agent-id", default=lambda: os.getenv("AGENT_ID", "unknown"), help="Agent identifier") +def commit(message: str, agent_id: str): + """Create a commit snapshot of current memory state.""" + import asyncio + + async def _commit(): + new_commit = await commit_service.create_commit( + message=message, + agent_id=agent_id, + ) + click.echo(f"Created commit {new_commit.hash[:8]}: {message}") + + asyncio.run(_commit()) + + +@cli.command() +@click.option("--limit", default=50, help="Maximum commits to show") +def log(limit: int): + """Show commit history.""" + import asyncio + + async def _log(): + commits = await commit_service.get_commits(limit=limit) + if not commits: + click.echo("No commits yet.") + return + for commit in commits: + click.echo(f"{commit.hash[:8]} - {commit.message}") + click.echo(f" Agent: {commit.agent_id} | {commit.created_at}") + click.echo() + + asyncio.run(_log()) + + +@cli.command() +@click.argument("hash1") +@click.argument("hash2") +def diff(hash1: str, hash2: str): + """Show diff between two commits.""" + import asyncio + + async def _diff(): + diff_result = await commit_service.diff_commits(hash1, hash2) + if diff_result["added"]: + click.echo("Added entries:") + for entry in diff_result["added"]: + click.echo(f" + {entry['title']}") + if diff_result["removed"]: + click.echo("Removed entries:") + for entry in diff_result["removed"]: + click.echo(f" - {entry['title']}") + if diff_result["modified"]: + click.echo("Modified entries:") + for mod in diff_result["modified"]: + click.echo(f" ~ {mod['before']['title']}") + if not any([diff_result["added"], diff_result["removed"], diff_result["modified"]]): + click.echo("No differences found.") + + asyncio.run(_diff()) + + +@cli.command() +@click.option("--host", default=lambda: os.getenv("MEMORY_API_HOST", "127.0.0.1"), help="Host to bind to") +@click.option("--port", default=lambda: int(os.getenv("MEMORY_API_PORT", "8080")), type=int, help="Port to bind to") +def serve(host: str, port: int): + """Start the API server.""" + uvicorn.run(app, host=host, port=port) + + +@cli.command() +def tui(): + """Launch the TUI dashboard.""" + from memory_manager.tui.app import run_tui + run_tui() + + +if __name__ == "__main__": + cli()