feat(cli): wire all subcommands
pyra (default→chat), pyra setup, pyra chat, pyra memory list/read/write/append All routes call bootstrap() first; PyraSecurityError exits with clear message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+100
@@ -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}")
|
||||||
Reference in New Issue
Block a user