mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
8df88e4d02
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
98 lines
3.5 KiB
Python
98 lines
3.5 KiB
Python
"""Watch command - run file watcher as a standalone long-running process."""
|
|
|
|
import asyncio
|
|
import os
|
|
import signal
|
|
import sys
|
|
from typing import Optional
|
|
|
|
import typer
|
|
from loguru import logger
|
|
|
|
from basic_memory import db
|
|
from basic_memory.cli.app import app
|
|
from basic_memory.cli.container import get_container
|
|
from basic_memory.config import ConfigManager
|
|
from basic_memory.services.initialization import initialize_app
|
|
from basic_memory.sync.coordinator import SyncCoordinator
|
|
|
|
|
|
async def run_watch(project: Optional[str] = None) -> None:
|
|
"""Run the file watcher as a long-running process.
|
|
|
|
This is the async core of the watch command. It:
|
|
1. Initializes the app (DB migrations + project reconciliation)
|
|
2. Validates and sets project constraint if --project given
|
|
3. Creates a SyncCoordinator with quiet=False for Rich console output
|
|
4. Blocks until SIGINT/SIGTERM, then shuts down cleanly
|
|
"""
|
|
container = get_container()
|
|
config = container.config
|
|
|
|
# --- Initialization ---
|
|
# Wrapped in try/finally so DB resources are cleaned up on all exit paths,
|
|
# including early exits from invalid --project names.
|
|
await initialize_app(config)
|
|
sync_coordinator = None
|
|
|
|
try:
|
|
# --- Project constraint ---
|
|
if project:
|
|
config_manager = ConfigManager()
|
|
project_name, _ = config_manager.get_project(project)
|
|
if not project_name:
|
|
typer.echo(f"No project found named: {project}", err=True)
|
|
raise typer.Exit(1)
|
|
|
|
os.environ["BASIC_MEMORY_MCP_PROJECT"] = project_name
|
|
logger.info(f"Watch constrained to project: {project_name}")
|
|
|
|
# --- Sync coordinator ---
|
|
# quiet=False so file change events are printed to the terminal
|
|
sync_coordinator = SyncCoordinator(config=config, should_sync=True, quiet=False)
|
|
|
|
# --- Signal handling ---
|
|
shutdown_event = asyncio.Event()
|
|
|
|
def _signal_handler() -> None:
|
|
logger.info("Shutdown signal received")
|
|
shutdown_event.set()
|
|
|
|
loop = asyncio.get_running_loop()
|
|
|
|
# Windows ProactorEventLoop does not support add_signal_handler;
|
|
# fall back to the stdlib signal module which works cross-platform.
|
|
try:
|
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
loop.add_signal_handler(sig, _signal_handler)
|
|
except NotImplementedError:
|
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
signal.signal(sig, lambda _signum, _frame: _signal_handler())
|
|
|
|
# --- Run ---
|
|
await sync_coordinator.start()
|
|
logger.info("Watch service running, press Ctrl+C to stop")
|
|
await shutdown_event.wait()
|
|
finally:
|
|
if sync_coordinator is not None:
|
|
await sync_coordinator.stop()
|
|
await db.shutdown_db()
|
|
logger.info("Watch service stopped")
|
|
|
|
|
|
@app.command()
|
|
def watch(
|
|
project: Optional[str] = typer.Option(None, help="Restrict watcher to a single project"),
|
|
) -> None:
|
|
"""Run file watcher as a long-running process (no MCP server).
|
|
|
|
Watches for file changes in project directories and syncs them to the
|
|
database. Useful for running Basic Memory sync alongside external tools
|
|
that don't use the MCP server.
|
|
"""
|
|
# On Windows, use SelectorEventLoop to avoid ProactorEventLoop cleanup issues
|
|
if sys.platform == "win32": # pragma: no cover
|
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
|
|
asyncio.run(run_watch(project=project))
|