diff --git a/src/pyra/cli.py b/src/pyra/cli.py new file mode 100644 index 0000000..4d0785c --- /dev/null +++ b/src/pyra/cli.py @@ -0,0 +1,100 @@ +import sys + +import click +from rich.console import Console + +from pyra.config.dirs import bootstrap +from pyra.security.boundaries import PyraSecurityError + +console = Console() + + +def _bootstrap_or_exit() -> None: + try: + bootstrap() + except PyraSecurityError as exc: + console.print(f"[bold red]Security error:[/bold red] {exc}") + sys.exit(1) + + +@click.group(invoke_without_command=True) +@click.pass_context +def main(ctx: click.Context) -> None: + """Pyra — personal AI assistant.""" + _bootstrap_or_exit() + if ctx.invoked_subcommand is None: + # Default to chat when no subcommand given + from pyra.chat.session import start_chat + start_chat() + + +@main.command() +def setup() -> None: + """Run the interactive provider setup wizard.""" + _bootstrap_or_exit() + from pyra.setup.wizard import run_setup + run_setup() + + +@main.command() +def chat() -> None: + """Start an interactive chat session.""" + _bootstrap_or_exit() + from pyra.chat.session import start_chat + start_chat() + + +@main.group() +def memory() -> None: + """Manage Pyra's long-term memory files.""" + _bootstrap_or_exit() + + +@memory.command("list") +def memory_list() -> None: + """List all memory files.""" + from pyra.memory.reader import list_memories + memories = list_memories() + if not memories: + console.print("[dim]No memory files found.[/dim]") + return + console.print(f"{'File':<45} {'Category':<14} {'Modified'}") + console.print("─" * 80) + for m in memories: + mtime = m.modified.strftime("%Y-%m-%d %H:%M") + console.print(f"{m.name:<45} {m.category:<14} {mtime}") + + +@memory.command("read") +@click.argument("name") +def memory_read(name: str) -> None: + """Read a memory file by name.""" + from pyra.memory.reader import read_memory + from pyra.security.boundaries import VaultAccessError + try: + content = read_memory(name) + console.print(content) + except VaultAccessError as exc: + console.print(f"[bold red]Blocked:[/bold red] {exc}") + except (FileNotFoundError, PermissionError) as exc: + console.print(f"[red]Error:[/red] {exc}") + + +@memory.command("write") +@click.argument("name") +@click.argument("content") +def memory_write(name: str, content: str) -> None: + """Write content to a memory file.""" + from pyra.memory.writer import write_memory + path = write_memory(name, content) + console.print(f"[green]Written:[/green] {path}") + + +@memory.command("append") +@click.argument("name") +@click.argument("content") +def memory_append(name: str, content: str) -> None: + """Append content to a memory file.""" + from pyra.memory.writer import append_memory + path = append_memory(name, content) + console.print(f"[green]Appended to:[/green] {path}")