mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
a368d06fd2
- Simplify `bm cloud status` output: remove verbose health check details (status/version/timestamp), show simple "Cloud connected" / "Cloud not connected" message instead - Improve `bm reindex --project` error for cloud projects: distinguish between "project not found" and "project is cloud-only" with a helpful message explaining reindexing is a local operation - Improve `bm project list` cloud error message: show the actual error and soften the credentials suggestion - Add tests for cloud status command (5 tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
231 lines
9.8 KiB
Python
231 lines
9.8 KiB
Python
"""Database management commands."""
|
|
|
|
from pathlib import Path
|
|
|
|
import typer
|
|
from loguru import logger
|
|
from rich.console import Console
|
|
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
|
|
from sqlalchemy.exc import OperationalError
|
|
|
|
from basic_memory import db
|
|
from basic_memory.cli.app import app
|
|
from basic_memory.cli.commands.command_utils import run_with_cleanup
|
|
from basic_memory.config import ConfigManager, ProjectMode
|
|
from basic_memory.repository import ProjectRepository
|
|
from basic_memory.services.initialization import reconcile_projects_with_config
|
|
from basic_memory.sync.sync_service import get_sync_service
|
|
|
|
console = Console()
|
|
|
|
|
|
async def _reindex_projects(app_config):
|
|
"""Reindex all projects in a single async context.
|
|
|
|
This ensures all database operations use the same event loop,
|
|
and proper cleanup happens when the function completes.
|
|
"""
|
|
try:
|
|
await reconcile_projects_with_config(app_config)
|
|
|
|
# Get database session (migrations already run if needed)
|
|
_, session_maker = await db.get_or_create_db(
|
|
db_path=app_config.database_path,
|
|
db_type=db.DatabaseType.FILESYSTEM,
|
|
)
|
|
project_repository = ProjectRepository(session_maker)
|
|
projects = await project_repository.get_active_projects()
|
|
|
|
for project in projects:
|
|
console.print(f" Indexing [cyan]{project.name}[/cyan]...")
|
|
logger.info(f"Starting sync for project: {project.name}")
|
|
sync_service = await get_sync_service(project)
|
|
sync_dir = Path(project.path)
|
|
await sync_service.sync(sync_dir, project_name=project.name)
|
|
logger.info(f"Sync completed for project: {project.name}")
|
|
finally:
|
|
# Clean up database connections before event loop closes
|
|
await db.shutdown_db()
|
|
|
|
|
|
@app.command()
|
|
def reset(
|
|
reindex: bool = typer.Option(False, "--reindex", help="Rebuild db index from filesystem"),
|
|
): # pragma: no cover
|
|
"""Reset database (drop all tables and recreate)."""
|
|
console.print(
|
|
"[yellow]Note:[/yellow] This only deletes the index database. "
|
|
"Your markdown note files will not be affected.\n"
|
|
"Use [green]bm reset --reindex[/green] to automatically rebuild the index afterward."
|
|
)
|
|
if typer.confirm("Reset the database index?"):
|
|
logger.info("Resetting database...")
|
|
config_manager = ConfigManager()
|
|
app_config = config_manager.config
|
|
# Get database path
|
|
db_path = app_config.app_database_path
|
|
|
|
# Delete the database file and WAL files if they exist
|
|
for suffix in ["", "-shm", "-wal"]:
|
|
path = db_path.parent / f"{db_path.name}{suffix}"
|
|
if path.exists():
|
|
try:
|
|
path.unlink()
|
|
logger.info(f"Deleted: {path}")
|
|
except OSError as e:
|
|
console.print(
|
|
f"[red]Error:[/red] Cannot delete {path.name}: {e}\n"
|
|
"The database may be in use by another process (e.g., MCP server).\n"
|
|
"Please close Claude Desktop or any other Basic Memory clients and try again."
|
|
)
|
|
raise typer.Exit(1)
|
|
|
|
# Create a new empty database (preserves project configuration)
|
|
try:
|
|
run_with_cleanup(db.run_migrations(app_config))
|
|
except OperationalError as e:
|
|
if "disk I/O error" in str(e) or "database is locked" in str(e):
|
|
console.print(
|
|
"[red]Error:[/red] Cannot access database. "
|
|
"It may be in use by another process (e.g., MCP server).\n"
|
|
"Please close Claude Desktop or any other Basic Memory clients and try again."
|
|
)
|
|
raise typer.Exit(1)
|
|
raise
|
|
console.print("[green]Database reset complete[/green]")
|
|
|
|
if reindex:
|
|
projects = list(app_config.projects)
|
|
if not projects:
|
|
console.print("[yellow]No projects configured. Skipping reindex.[/yellow]")
|
|
else:
|
|
console.print(f"Rebuilding search index for {len(projects)} project(s)...")
|
|
# Note: _reindex_projects has its own cleanup, but run_with_cleanup
|
|
# ensures db.shutdown_db() is called even if _reindex_projects changes
|
|
run_with_cleanup(_reindex_projects(app_config))
|
|
console.print("[green]Reindex complete[/green]")
|
|
|
|
|
|
@app.command()
|
|
def reindex(
|
|
embeddings: bool = typer.Option(
|
|
False, "--embeddings", "-e", help="Rebuild vector embeddings (requires semantic search)"
|
|
),
|
|
search: bool = typer.Option(False, "--search", "-s", help="Rebuild full-text search index"),
|
|
project: str = typer.Option(
|
|
None, "--project", "-p", help="Reindex a specific project (default: all)"
|
|
),
|
|
): # pragma: no cover
|
|
"""Rebuild search indexes and/or vector embeddings without dropping the database.
|
|
|
|
By default rebuilds everything (search + embeddings if semantic is enabled).
|
|
Use --search or --embeddings to rebuild only one.
|
|
|
|
Examples:
|
|
bm reindex # Rebuild everything
|
|
bm reindex --embeddings # Only rebuild vector embeddings
|
|
bm reindex --search # Only rebuild FTS index
|
|
bm reindex -p claw # Reindex only the 'claw' project
|
|
"""
|
|
# If neither flag is set, do both
|
|
if not embeddings and not search:
|
|
embeddings = True
|
|
search = True
|
|
|
|
config_manager = ConfigManager()
|
|
app_config = config_manager.config
|
|
|
|
if embeddings and not app_config.semantic_search_enabled:
|
|
console.print(
|
|
"[yellow]Semantic search is not enabled.[/yellow] "
|
|
"Set [cyan]semantic_search_enabled: true[/cyan] in config to use embeddings."
|
|
)
|
|
embeddings = False
|
|
if not search:
|
|
raise typer.Exit(0)
|
|
|
|
run_with_cleanup(_reindex(app_config, search=search, embeddings=embeddings, project=project))
|
|
|
|
|
|
async def _reindex(app_config, search: bool, embeddings: bool, project: str | None):
|
|
"""Run reindex operations."""
|
|
from basic_memory.repository import EntityRepository
|
|
from basic_memory.repository.search_repository import create_search_repository
|
|
from basic_memory.services.search_service import SearchService
|
|
from basic_memory.services.file_service import FileService
|
|
from basic_memory.markdown.markdown_processor import MarkdownProcessor
|
|
from basic_memory.markdown.entity_parser import EntityParser
|
|
|
|
try:
|
|
await reconcile_projects_with_config(app_config)
|
|
|
|
_, session_maker = await db.get_or_create_db(
|
|
db_path=app_config.database_path,
|
|
db_type=db.DatabaseType.FILESYSTEM,
|
|
)
|
|
project_repository = ProjectRepository(session_maker)
|
|
projects = await project_repository.get_active_projects()
|
|
|
|
if project:
|
|
projects = [p for p in projects if p.name == project]
|
|
if not projects:
|
|
# Check if it's a cloud-only project — those can't be reindexed locally
|
|
project_mode = app_config.get_project_mode(project)
|
|
if project_mode == ProjectMode.CLOUD:
|
|
console.print(
|
|
f"[yellow]Project '{project}' is a cloud project.[/yellow]\n"
|
|
"Reindexing is a local operation — cloud projects are "
|
|
"indexed on the server."
|
|
)
|
|
else:
|
|
console.print(f"[red]Project '{project}' not found.[/red]")
|
|
raise typer.Exit(1)
|
|
|
|
for proj in projects:
|
|
console.print(f"\n[bold]Project: [cyan]{proj.name}[/cyan][/bold]")
|
|
|
|
if search:
|
|
console.print(" Rebuilding full-text search index...")
|
|
sync_service = await get_sync_service(proj)
|
|
sync_dir = Path(proj.path)
|
|
await sync_service.sync(sync_dir, project_name=proj.name)
|
|
console.print(" [green]✓[/green] Full-text search index rebuilt")
|
|
|
|
if embeddings:
|
|
console.print(" Building vector embeddings...")
|
|
entity_repository = EntityRepository(session_maker, project_id=proj.id)
|
|
search_repository = create_search_repository(
|
|
session_maker, project_id=proj.id, app_config=app_config
|
|
)
|
|
project_path = Path(proj.path)
|
|
entity_parser = EntityParser(project_path)
|
|
markdown_processor = MarkdownProcessor(entity_parser, app_config=app_config)
|
|
file_service = FileService(project_path, markdown_processor, app_config=app_config)
|
|
search_service = SearchService(search_repository, entity_repository, file_service)
|
|
|
|
with Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
BarColumn(),
|
|
TaskProgressColumn(),
|
|
console=console,
|
|
) as progress:
|
|
task = progress.add_task(" Embedding entities...", total=None)
|
|
|
|
def on_progress(entity_id, index, total):
|
|
progress.update(task, total=total, completed=index)
|
|
|
|
stats = await search_service.reindex_vectors(progress_callback=on_progress)
|
|
progress.update(task, completed=stats["total_entities"])
|
|
|
|
console.print(
|
|
f" [green]✓[/green] Embeddings complete: "
|
|
f"{stats['embedded']} entities embedded, "
|
|
f"{stats['skipped']} skipped, "
|
|
f"{stats['errors']} errors"
|
|
)
|
|
|
|
console.print("\n[green]Reindex complete![/green]")
|
|
finally:
|
|
await db.shutdown_db()
|