diff --git a/src/basic_memory/api/app.py b/src/basic_memory/api/app.py index 9daac37f..12522da6 100644 --- a/src/basic_memory/api/app.py +++ b/src/basic_memory/api/app.py @@ -1,5 +1,6 @@ """FastAPI application for basic-memory knowledge graph API.""" +import asyncio from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException @@ -7,16 +8,54 @@ from fastapi.exception_handlers import http_exception_handler from loguru import logger from basic_memory import db -from basic_memory.config import config as app_config -from basic_memory.api.routers import knowledge, search, memory, resource, project_info +from basic_memory.api.routers import knowledge, memory, project_info, resource, search +from basic_memory.config import config as project_config +from basic_memory.config import config_manager +from basic_memory.sync import SyncService, WatchService + + +async def run_background_sync(sync_service: SyncService, watch_service: WatchService): # pragma: no cover + logger.info(f"Starting watch service to sync file changes in dir: {project_config.home}") + # full sync + await sync_service.sync(project_config.home, show_progress=False) + + # watch changes + await watch_service.run() @asynccontextmanager async def lifespan(app: FastAPI): # pragma: no cover """Lifecycle manager for the FastAPI app.""" - await db.run_migrations(app_config) + await db.run_migrations(project_config) + + # app config + basic_memory_config = config_manager.load_config() + logger.info(f"Sync changes enabled: {basic_memory_config.sync_changes}") + logger.info(f"Update permalinks on move enabled: {basic_memory_config.update_permalinks_on_move}") + + watch_task = None + if basic_memory_config.sync_changes: + # import after migrations have run + from basic_memory.cli.commands.sync import get_sync_service + + sync_service = await get_sync_service() + watch_service = WatchService( + sync_service=sync_service, + file_service=sync_service.entity_service.file_service, + config=project_config, + ) + watch_task = asyncio.create_task(run_background_sync(sync_service, watch_service)) + else: + logger.info("Sync changes disabled. Skipping watch service.") + + + # proceed with startup yield + logger.info("Shutting down Basic Memory API") + if watch_task: + watch_task.cancel() + await db.shutdown_db() diff --git a/src/basic_memory/cli/app.py b/src/basic_memory/cli/app.py index 81e7029a..92a4e89b 100644 --- a/src/basic_memory/cli/app.py +++ b/src/basic_memory/cli/app.py @@ -1,11 +1,7 @@ -import asyncio from typing import Optional import typer -from basic_memory import db -from basic_memory.config import config - def version_callback(value: bool) -> None: """Show version and exit.""" @@ -58,9 +54,6 @@ def app_callback( config = new_config -# Run database migrations -asyncio.run(db.run_migrations(config)) - # Register sub-command groups import_app = typer.Typer(help="Import data from various sources") app.add_typer(import_app, name="import") diff --git a/src/basic_memory/cli/commands/mcp.py b/src/basic_memory/cli/commands/mcp.py index d347c821..6a211e13 100644 --- a/src/basic_memory/cli/commands/mcp.py +++ b/src/basic_memory/cli/commands/mcp.py @@ -4,7 +4,7 @@ from loguru import logger import basic_memory from basic_memory.cli.app import app -from basic_memory.config import config +from basic_memory.config import config, config_manager # Import mcp instance from basic_memory.mcp.server import mcp as mcp_server # pragma: no cover @@ -19,8 +19,15 @@ def mcp(): # pragma: no cover home_dir = config.home project_name = config.project + # app config + basic_memory_config = config_manager.load_config() + logger.info(f"Starting Basic Memory MCP server {basic_memory.__version__}") logger.info(f"Project: {project_name}") logger.info(f"Project directory: {home_dir}") + logger.info(f"Sync changes enabled: {basic_memory_config.sync_changes}") + logger.info( + f"Update permalinks on move enabled: {basic_memory_config.update_permalinks_on_move}" + ) mcp_server.run() diff --git a/src/basic_memory/cli/main.py b/src/basic_memory/cli/main.py index 25320630..8338cb84 100644 --- a/src/basic_memory/cli/main.py +++ b/src/basic_memory/cli/main.py @@ -1,21 +1,26 @@ """Main CLI entry point for basic-memory.""" # pragma: no cover -from basic_memory.cli.app import app # pragma: no cover +import asyncio + import typer +from basic_memory.cli.app import app # pragma: no cover + # Register commands from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover - status, - sync, db, - import_memory_json, - mcp, + import_chatgpt, import_claude_conversations, import_claude_projects, - import_chatgpt, - tool, + import_memory_json, + mcp, project, + status, + sync, + tool, ) +from basic_memory.config import config +from basic_memory.db import run_migrations as db_run_migrations # Version command @@ -55,4 +60,8 @@ def main( if __name__ == "__main__": # pragma: no cover + # Run database migrations + asyncio.run(db_run_migrations(config)) + + # start the app app() diff --git a/src/basic_memory/config.py b/src/basic_memory/config.py index d078262c..854b24c6 100644 --- a/src/basic_memory/config.py +++ b/src/basic_memory/config.py @@ -38,8 +38,7 @@ class ProjectConfig(BaseSettings): default=500, description="Milliseconds to wait after changes before syncing", gt=0 ) - log_level: str = "DEBUG" - + # update permalinks on move update_permalinks_on_move: bool = Field( default=False, description="Whether to update permalinks when files are moved or renamed. default (False)", @@ -82,6 +81,18 @@ class BasicMemoryConfig(BaseSettings): description="Name of the default project to use", ) + log_level: str = "INFO" + + update_permalinks_on_move: bool = Field( + default=False, + description="Whether to update permalinks when files are moved or renamed. default (False)", + ) + + sync_changes: bool = Field( + default=True, + description="Whether to sync changes in real time. default (True)", + ) + model_config = SettingsConfigDict( env_prefix="BASIC_MEMORY_", extra="ignore", @@ -199,9 +210,14 @@ def get_project_config(project_name: Optional[str] = None) -> ProjectConfig: "BASIC_MEMORY_PROJECT", project_name or config_manager.default_project ) + update_permalinks_on_move = config_manager.load_config().update_permalinks_on_move try: project_path = config_manager.get_project_path(actual_project_name) - return ProjectConfig(home=project_path, project=actual_project_name) + return ProjectConfig( + home=project_path, + project=actual_project_name, + update_permalinks_on_move=update_permalinks_on_move, + ) except ValueError: # pragma: no cover logger.warning(f"Project '{actual_project_name}' not found, using default") project_path = config_manager.get_project_path(config_manager.default_project) @@ -230,8 +246,10 @@ def get_process_name(): # pragma: no cover return "sync" elif "mcp" in sys.argv: return "mcp" - else: + elif "cli" in sys.argv: return "cli" + else: + return "api" process_name = get_process_name() @@ -251,7 +269,7 @@ def setup_basic_memory_logging(): # pragma: no cover setup_logging( env=config.env, home_dir=user_home, # Use user home for logs - log_level=config.log_level, + log_level=config_manager.load_config().log_level, log_file=f"{DATA_DIR_NAME}/basic-memory-{process_name}.log", console=False, ) diff --git a/src/basic_memory/db.py b/src/basic_memory/db.py index 615c9f2e..ccf47c5e 100644 --- a/src/basic_memory/db.py +++ b/src/basic_memory/db.py @@ -146,7 +146,9 @@ async def engine_session_factory( _session_maker = None -async def run_migrations(app_config: ProjectConfig, database_type=DatabaseType.FILESYSTEM): +async def run_migrations( + app_config: ProjectConfig, database_type=DatabaseType.FILESYSTEM +): # pragma: no cover """Run any pending alembic migrations.""" logger.info("Running database migrations...") try: