# This prevents DEBUG logs from appearing on stdout during module-level # initialization (e.g., template_loader.TemplateLoader() logs at DEBUG level). from loguru import logger logger.remove() from typing import Optional # noqa: E402 import typer # noqa: E402 from basic_memory.cli.auto_update import maybe_run_periodic_auto_update # noqa: E402 from basic_memory.cli.container import CliContainer, set_container # noqa: E402 from basic_memory.cli.promo import maybe_show_cloud_promo, maybe_show_init_line # noqa: E402 from basic_memory.config import init_cli_logging # noqa: E402 import logfire # noqa: E402 def version_callback(value: bool) -> None: """Show version and exit.""" if value: # pragma: no cover import basic_memory typer.echo(f"Basic Memory version: {basic_memory.__version__}") raise typer.Exit() app = typer.Typer(name="basic-memory") @app.callback() def app_callback( ctx: typer.Context, version: Optional[bool] = typer.Option( None, "--version", "-v", help="Show version and exit.", callback=version_callback, is_eager=True, ), ) -> None: """Basic Memory - Local-first personal knowledge management.""" # Initialize logging for CLI (file only, no stdout) init_cli_logging() command_name = ctx.invoked_subcommand or "root" ctx.with_resource( logfire.span( f"cli.command.{command_name}", entrypoint="cli", command_name=command_name, ) ) # --- Composition Root --- # Create container and read config (single point of config access) container = CliContainer.create() set_container(container) # Trigger: first-run init confirmation before command output. # Why: informational "initialized" message belongs above command results, not in the upsell panel. # Outcome: one-time plain line printed before the subcommand runs. maybe_show_init_line(ctx.invoked_subcommand) # Trigger: register post-command messaging callbacks. # Why: informational/promo/update output belongs below command results. # Outcome: command output remains primary, with optional follow-up notices afterwards. def _post_command_messages() -> None: maybe_show_cloud_promo(ctx.invoked_subcommand) maybe_run_periodic_auto_update(ctx.invoked_subcommand) ctx.call_on_close(_post_command_messages) # Run initialization for commands that don't use the API # Skip for 'mcp' command - it has its own lifespan that handles initialization # Skip for API-using commands (status, sync, etc.) - they handle initialization via deps.py # Skip for 'reset' command - it manages its own database lifecycle skip_init_commands = { "doctor", "mcp", "status", "sync", "project", "tool", "reset", "reindex", "update", "watch", } if ( not version and ctx.invoked_subcommand is not None and ctx.invoked_subcommand not in skip_init_commands ): from basic_memory.services.initialization import ensure_initialization ensure_initialization(container.config) ## import # Register sub-command groups import_app = typer.Typer(help="Import data from various sources") app.add_typer(import_app, name="import") claude_app = typer.Typer(help="Import Conversations from Claude JSON export.") import_app.add_typer(claude_app, name="claude") ## cloud cloud_app = typer.Typer(help="Access Basic Memory Cloud") app.add_typer(cloud_app, name="cloud")