Skip to content

Commit 030158a

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/optional-semantic-dependencies
Signed-off-by: phernandez <paul@basicmachines.co> # Conflicts: # src/basic_memory/repository/openai_provider.py # src/basic_memory/repository/sqlite_search_repository.py
2 parents 5aff765 + ed94877 commit 030158a

19 files changed

Lines changed: 590 additions & 74 deletions

src/basic_memory/cli/app.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,17 @@ def app_callback(
5050
# Skip for 'mcp' command - it has its own lifespan that handles initialization
5151
# Skip for API-using commands (status, sync, etc.) - they handle initialization via deps.py
5252
# Skip for 'reset' command - it manages its own database lifecycle
53-
skip_init_commands = {"doctor", "mcp", "status", "sync", "project", "tool", "reset", "reindex"}
53+
skip_init_commands = {
54+
"doctor",
55+
"mcp",
56+
"status",
57+
"sync",
58+
"project",
59+
"tool",
60+
"reset",
61+
"reindex",
62+
"watch",
63+
}
5464
if (
5565
not version
5666
and ctx.invoked_subcommand is not None

src/basic_memory/cli/commands/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""CLI commands for basic-memory."""
22

33
from . import status, db, doctor, import_memory_json, mcp, import_claude_conversations
4-
from . import import_claude_projects, import_chatgpt, tool, project, format, schema
4+
from . import import_claude_projects, import_chatgpt, tool, project, format, schema, watch
55

66
__all__ = [
77
"status",
@@ -16,4 +16,5 @@
1616
"project",
1717
"format",
1818
"schema",
19+
"watch",
1920
]
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Watch command - run file watcher as a standalone long-running process."""
2+
3+
import asyncio
4+
import os
5+
import signal
6+
import sys
7+
from typing import Optional
8+
9+
import typer
10+
from loguru import logger
11+
12+
from basic_memory import db
13+
from basic_memory.cli.app import app
14+
from basic_memory.cli.container import get_container
15+
from basic_memory.config import ConfigManager
16+
from basic_memory.services.initialization import initialize_app
17+
from basic_memory.sync.coordinator import SyncCoordinator
18+
19+
20+
async def run_watch(project: Optional[str] = None) -> None:
21+
"""Run the file watcher as a long-running process.
22+
23+
This is the async core of the watch command. It:
24+
1. Initializes the app (DB migrations + project reconciliation)
25+
2. Validates and sets project constraint if --project given
26+
3. Creates a SyncCoordinator with quiet=False for Rich console output
27+
4. Blocks until SIGINT/SIGTERM, then shuts down cleanly
28+
"""
29+
container = get_container()
30+
config = container.config
31+
32+
# --- Initialization ---
33+
# Wrapped in try/finally so DB resources are cleaned up on all exit paths,
34+
# including early exits from invalid --project names.
35+
await initialize_app(config)
36+
sync_coordinator = None
37+
38+
try:
39+
# --- Project constraint ---
40+
if project:
41+
config_manager = ConfigManager()
42+
project_name, _ = config_manager.get_project(project)
43+
if not project_name:
44+
typer.echo(f"No project found named: {project}", err=True)
45+
raise typer.Exit(1)
46+
47+
os.environ["BASIC_MEMORY_MCP_PROJECT"] = project_name
48+
logger.info(f"Watch constrained to project: {project_name}")
49+
50+
# --- Sync coordinator ---
51+
# quiet=False so file change events are printed to the terminal
52+
sync_coordinator = SyncCoordinator(config=config, should_sync=True, quiet=False)
53+
54+
# --- Signal handling ---
55+
shutdown_event = asyncio.Event()
56+
57+
def _signal_handler() -> None:
58+
logger.info("Shutdown signal received")
59+
shutdown_event.set()
60+
61+
loop = asyncio.get_running_loop()
62+
63+
# Windows ProactorEventLoop does not support add_signal_handler;
64+
# fall back to the stdlib signal module which works cross-platform.
65+
try:
66+
for sig in (signal.SIGINT, signal.SIGTERM):
67+
loop.add_signal_handler(sig, _signal_handler)
68+
except NotImplementedError:
69+
for sig in (signal.SIGINT, signal.SIGTERM):
70+
signal.signal(sig, lambda _signum, _frame: _signal_handler())
71+
72+
# --- Run ---
73+
await sync_coordinator.start()
74+
logger.info("Watch service running, press Ctrl+C to stop")
75+
await shutdown_event.wait()
76+
finally:
77+
if sync_coordinator is not None:
78+
await sync_coordinator.stop()
79+
await db.shutdown_db()
80+
logger.info("Watch service stopped")
81+
82+
83+
@app.command()
84+
def watch(
85+
project: Optional[str] = typer.Option(None, help="Restrict watcher to a single project"),
86+
) -> None:
87+
"""Run file watcher as a long-running process (no MCP server).
88+
89+
Watches for file changes in project directories and syncs them to the
90+
database. Useful for running Basic Memory sync alongside external tools
91+
that don't use the MCP server.
92+
"""
93+
# On Windows, use SelectorEventLoop to avoid ProactorEventLoop cleanup issues
94+
if sys.platform == "win32": # pragma: no cover
95+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
96+
97+
asyncio.run(run_watch(project=project))

src/basic_memory/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class BasicMemoryConfig(BaseSettings):
8181
description="Name of the default project to use",
8282
)
8383
default_project_mode: bool = Field(
84-
default=False,
84+
default=True,
8585
description="When True, MCP tools automatically use default_project when no project parameter is specified. Enables simplified UX for single-project workflows.",
8686
)
8787

src/basic_memory/mcp/project_context.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,17 @@ async def resolve_project_parameter(
2929
default_project_mode: Optional[bool] = None,
3030
default_project: Optional[str] = None,
3131
) -> Optional[str]:
32-
"""Resolve project parameter using three-tier hierarchy.
32+
"""Resolve project parameter using unified linear priority chain.
3333
3434
This is a thin wrapper around ProjectResolver for backwards compatibility.
3535
New code should consider using ProjectResolver directly for more detailed
3636
resolution information.
3737
38-
if cloud_mode:
39-
project is required (unless allow_discovery=True for tools that support discovery mode)
40-
else:
41-
Resolution order:
42-
1. Single Project Mode (--project cli arg, or BASIC_MEMORY_MCP_PROJECT env var) - highest priority
43-
2. Explicit project parameter - medium priority
44-
3. Default project if default_project_mode=true - lowest priority
38+
Resolution order (same for local and cloud modes):
39+
1. ENV_CONSTRAINT: BASIC_MEMORY_MCP_PROJECT env var (highest priority)
40+
2. EXPLICIT: project parameter passed directly
41+
3. DEFAULT: default project when default_project_mode=true
42+
4. Fallback: cloud → CLOUD_DISCOVERY or ValueError; local → NONE
4543
4644
Args:
4745
project: Optional explicit project parameter

src/basic_memory/mcp/prompts/ai_assistant_guide.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def ai_assistant_guide() -> str:
3232

3333
# Add mode-specific header
3434
mode_info = ""
35-
if config.default_project_mode: # pragma: no cover
35+
if config.default_project_mode:
3636
mode_info = f"""
3737
# 🎯 Default Project Mode Active
3838

src/basic_memory/mcp/resources/ai_assistant_guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ await write_note("Note", "Content", "folder")
4343
await write_note("Note", "Content", "folder", project="main")
4444
```
4545

46-
When `default_project_mode=false` (default):
46+
When `default_project_mode=false`:
4747
```python
4848
# Project required:
4949
await write_note("Note", "Content", "folder", project="main") #

src/basic_memory/mcp/tools/build_context.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ async def build_context(
5050
a rich context graph of related information.
5151
5252
Project Resolution:
53-
Server resolves projects in this order: Single Project Mode → project parameter → default project.
54-
If project unknown, use list_memory_projects() or recent_activity() first.
53+
Server resolves projects using a unified priority chain (same in local and cloud modes):
54+
Single Project Mode → project parameter → default project.
55+
Uses default project automatically. Specify `project` parameter to target a different project.
5556
5657
Args:
5758
project: Project name to build context from. Optional - server will resolve using hierarchy.

src/basic_memory/mcp/tools/read_note.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ async def read_note(
3030
returning the raw markdown content including observations, relations, and metadata.
3131
3232
Project Resolution:
33-
Server resolves projects in this order: Single Project Mode → project parameter → default project.
34-
If project unknown, use list_memory_projects() or recent_activity() first.
33+
Server resolves projects using a unified priority chain (same in local and cloud modes):
34+
Single Project Mode → project parameter → default project.
35+
Uses default project automatically. Specify `project` parameter to target a different project.
3536
3637
This tool will try multiple lookup strategies to find the most relevant note:
3738
1. Direct permalink lookup

src/basic_memory/mcp/tools/write_note.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ async def write_note(
3232
Creates or updates a markdown note with semantic observations and relations.
3333
3434
Project Resolution:
35-
Server resolves projects in this order: Single Project Mode → project parameter → default project.
36-
If project unknown, use list_memory_projects() or recent_activity() first.
35+
Server resolves projects using a unified priority chain (same in local and cloud modes):
36+
Single Project Mode → project parameter → default project.
37+
Uses default project automatically. Specify `project` parameter to target a different project.
3738
3839
The content can include semantic observations and relations using markdown syntax:
3940
@@ -79,12 +80,7 @@ async def write_note(
7980
- Session tracking metadata for project awareness
8081
8182
Examples:
82-
# Assistant flow when project is unknown
83-
# 1. list_memory_projects() -> Ask user which project
84-
# 2. User: "Use my-research"
85-
# 3. write_note(...) and remember "my-research" for session
86-
87-
# Create a simple note
83+
# Create a simple note (uses default project automatically)
8884
write_note(
8985
project="my-research",
9086
title="Meeting Notes",

0 commit comments

Comments
 (0)