Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/basic_memory/cli/commands/ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
from basic_memory.cli.app import app
from basic_memory.cli.commands.command_utils import run_with_cleanup
from basic_memory.cli.commands.routing import force_routing, validate_routing_flags
from basic_memory.mcp.tools import search_notes as mcp_search_notes
from basic_memory.mcp.tools import write_note as mcp_write_note

# MCP tool functions are imported inside the async helpers below: importing
# basic_memory.mcp.tools loads the entire tool stack (fastmcp, mcp SDK,
# SQLAlchemy), which would slow every CLI invocation, including --help (#886).


console = Console()
Expand Down Expand Up @@ -252,6 +254,10 @@ async def seed_project_update_schemas(
refresh: bool = False,
) -> list[str]:
"""Seed Auto BM schema notes without overwriting customized schemas."""
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
from basic_memory.mcp.tools import search_notes as mcp_search_notes
from basic_memory.mcp.tools import write_note as mcp_write_note

seeded: list[str] = []
routed_project = _routed_project(project=project, project_id=project_id, workspace=workspace)
for spec in schema_seed_specs():
Expand Down Expand Up @@ -295,6 +301,10 @@ async def publish_project_update_note(
note: ProjectUpdateNote,
) -> dict[str, Any]:
"""Search by idempotency key and then upsert the deterministic note path."""
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
from basic_memory.mcp.tools import search_notes as mcp_search_notes
from basic_memory.mcp.tools import write_note as mcp_write_note

routed_project = _routed_project(
project=config.project,
project_id=config.project_id,
Expand Down
10 changes: 8 additions & 2 deletions src/basic_memory/cli/commands/command_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
import asyncio
from typing import Optional, TypeVar, Coroutine, Any

from mcp.server.fastmcp.exceptions import ToolError
import typer

from rich.console import Console

from basic_memory import db
from basic_memory.config import ConfigManager
from basic_memory.mcp.async_client import get_client
from basic_memory.mcp.clients import ProjectClient
Expand All @@ -31,6 +29,9 @@ def run_with_cleanup(coro: Coroutine[Any, Any, T]) -> T:
Returns:
The result of the coroutine
"""
# Deferred: basic_memory.db pulls SQLAlchemy + Alembic, which must not load
# at CLI import time — only when a command actually runs (#886).
from basic_memory import db

async def _with_cleanup() -> T:
try:
Expand All @@ -53,6 +54,8 @@ async def run_sync(
force_full: If True, force a full scan bypassing watermark optimization
run_in_background: If True, return immediately; if False, wait for completion
"""
# Deferred: ToolError lives in the mcp SDK, which must not load at CLI startup (#886).
from mcp.server.fastmcp.exceptions import ToolError

# Resolve default project so get_client() can route per-project
project = project or ConfigManager().default_project
Expand Down Expand Up @@ -86,6 +89,9 @@ async def run_sync(

async def get_project_info(project: str):
"""Get project information via API endpoint."""
# Deferred: ToolError lives in the mcp SDK, which must not load at CLI startup (#886).
from mcp.server.fastmcp.exceptions import ToolError

try:
async with get_client(project_name=project) as client:
project_item = await get_active_project(client, project, None)
Expand Down
35 changes: 28 additions & 7 deletions src/basic_memory/cli/commands/db.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
"""Database management commands."""

# PEP 563 lazy annotations let signatures reference IndexProgress without importing
# the indexing stack at module load; reset/reindex import their heavy database and
# sync dependencies at call time so CLI startup stays fast (#886).
from __future__ import annotations

import os
from dataclasses import dataclass
from pathlib import Path, PurePosixPath, PureWindowsPath
from typing import TYPE_CHECKING

import psutil
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.indexing import IndexProgress
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

if TYPE_CHECKING:
from basic_memory.indexing import IndexProgress

console = Console()

Expand Down Expand Up @@ -159,6 +162,13 @@ async def _reindex_projects(app_config):
This ensures all database operations use the same event loop,
and proper cleanup happens when the function completes.
"""
# Deferred: SQLAlchemy, repositories, and the sync stack load only when a
# reindex actually runs, not on every CLI start (#886).
from basic_memory import db
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

try:
await reconcile_projects_with_config(app_config)

Expand Down Expand Up @@ -197,6 +207,12 @@ def reset(
),
): # pragma: no cover
"""Reset database (drop all tables and recreate)."""
# Deferred: SQLAlchemy and the db module load only when a reset actually
# runs, not on every CLI start (#886).
from sqlalchemy.exc import OperationalError

from basic_memory import db

console.print(
"[yellow]Note:[/yellow] This only deletes the index database. "
"Your markdown note files will not be affected.\n"
Expand Down Expand Up @@ -320,10 +336,15 @@ async def _reindex(
project: str | None,
):
"""Run reindex operations."""
from basic_memory.repository import EntityRepository
# Deferred: SQLAlchemy, repositories, and the sync stack load only when a
# reindex actually runs, not on every CLI start (#886).
from basic_memory import db
from basic_memory.repository import EntityRepository, ProjectRepository
from basic_memory.repository.search_repository import create_search_repository
from basic_memory.services.initialization import reconcile_projects_with_config
from basic_memory.services.search_service import SearchService
from basic_memory.services.file_service import FileService
from basic_memory.sync.sync_service import get_sync_service
from basic_memory.markdown.markdown_processor import MarkdownProcessor
from basic_memory.markdown.entity_parser import EntityParser

Expand Down
13 changes: 9 additions & 4 deletions src/basic_memory/cli/commands/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,12 @@
from pathlib import Path

from loguru import logger
from mcp.server.fastmcp.exceptions import ToolError
from rich.console import Console
import typer

from basic_memory.cli.app import app
from basic_memory.cli.commands.command_utils import run_with_cleanup
from basic_memory.cli.commands.routing import force_routing, validate_routing_flags
from basic_memory.markdown.entity_parser import EntityParser
from basic_memory.markdown.markdown_processor import MarkdownProcessor
from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown
from basic_memory.mcp.async_client import get_client
from basic_memory.mcp.clients import KnowledgeClient, ProjectClient, SearchClient
from basic_memory.schemas.base import Entity
Expand All @@ -29,6 +25,12 @@

async def run_doctor() -> None:
"""Run local consistency checks for file <-> database flows."""
# Deferred: the markdown parsing stack is only needed while the checks run,
# and importing it at module level slows every CLI invocation (#886).
from basic_memory.markdown.entity_parser import EntityParser
from basic_memory.markdown.markdown_processor import MarkdownProcessor
from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown

console.print("[blue]Running Basic Memory doctor checks...[/blue]")

project_name = f"doctor-{uuid.uuid4().hex[:8]}"
Expand Down Expand Up @@ -140,6 +142,9 @@ def doctor(
cloud: bool = typer.Option(False, "--cloud", help="Force cloud API routing"),
) -> None:
"""Run local consistency checks to verify file/database sync."""
# Deferred: ToolError lives in the mcp SDK, which must not load at CLI startup (#886).
from mcp.server.fastmcp.exceptions import ToolError

try:
validate_routing_flags(local, cloud)
# Doctor runs local filesystem checks — always default to local routing
Expand Down
20 changes: 16 additions & 4 deletions src/basic_memory/cli/commands/import_chatgpt.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
"""Import command for ChatGPT conversations."""

# PEP 563 lazy annotations keep heavy importer types out of module import (#886).
from __future__ import annotations

import json
from pathlib import Path
from typing import Annotated, Tuple
from typing import TYPE_CHECKING, Annotated, Tuple

import typer
from basic_memory.cli.app import import_app
from basic_memory.cli.commands.command_utils import run_with_cleanup
from basic_memory.config import ConfigManager, get_project_config
from basic_memory.importers import ChatGPTImporter
from basic_memory.markdown import EntityParser, MarkdownProcessor
from basic_memory.services.file_service import FileService
from loguru import logger
from rich.console import Console
from rich.panel import Panel

if TYPE_CHECKING:
from basic_memory.markdown import MarkdownProcessor
from basic_memory.services.file_service import FileService

console = Console()


async def get_importer_dependencies() -> Tuple[MarkdownProcessor, FileService]:
"""Get MarkdownProcessor and FileService instances for importers."""
# Deferred: the markdown/file-service stack pulls SQLAlchemy and must load
# only when an import actually runs, not on every CLI start (#886).
from basic_memory.markdown import EntityParser, MarkdownProcessor
from basic_memory.services.file_service import FileService

config = get_project_config()
app_config = ConfigManager().config
entity_parser = EntityParser(config.home)
Expand Down Expand Up @@ -60,6 +69,9 @@ def import_chatgpt(
console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")

# Create importer and run import
# Deferred: importer stack loads at import-command run time only (#886).
from basic_memory.importers import ChatGPTImporter

importer = ChatGPTImporter(
config.home, markdown_processor, file_service, project_name=config.name
)
Expand Down
20 changes: 16 additions & 4 deletions src/basic_memory/cli/commands/import_claude_conversations.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
"""Import command for basic-memory CLI to import chat data from conversations2.json format."""

# PEP 563 lazy annotations keep heavy importer types out of module import (#886).
from __future__ import annotations

import json
from pathlib import Path
from typing import Annotated, Tuple
from typing import TYPE_CHECKING, Annotated, Tuple

import typer
from basic_memory.cli.app import claude_app
from basic_memory.cli.commands.command_utils import run_with_cleanup
from basic_memory.config import ConfigManager, get_project_config
from basic_memory.importers.claude_conversations_importer import ClaudeConversationsImporter
from basic_memory.markdown import EntityParser, MarkdownProcessor
from basic_memory.services.file_service import FileService
from loguru import logger
from rich.console import Console
from rich.panel import Panel

if TYPE_CHECKING:
from basic_memory.markdown import MarkdownProcessor
from basic_memory.services.file_service import FileService

console = Console()


async def get_importer_dependencies() -> Tuple[MarkdownProcessor, FileService]:
"""Get MarkdownProcessor and FileService instances for importers."""
# Deferred: the markdown/file-service stack pulls SQLAlchemy and must load
# only when an import actually runs, not on every CLI start (#886).
from basic_memory.markdown import EntityParser, MarkdownProcessor
from basic_memory.services.file_service import FileService

config = get_project_config()
app_config = ConfigManager().config
entity_parser = EntityParser(config.home)
Expand Down Expand Up @@ -57,6 +66,9 @@ def import_claude(
markdown_processor, file_service = run_with_cleanup(get_importer_dependencies())

# Create the importer
# Deferred: importer stack loads at import-command run time only (#886).
from basic_memory.importers.claude_conversations_importer import ClaudeConversationsImporter

importer = ClaudeConversationsImporter(
config.home, markdown_processor, file_service, project_name=config.name
)
Expand Down
20 changes: 16 additions & 4 deletions src/basic_memory/cli/commands/import_claude_projects.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
"""Import command for basic-memory CLI to import project data from Claude.ai."""

# PEP 563 lazy annotations keep heavy importer types out of module import (#886).
from __future__ import annotations

import json
from pathlib import Path
from typing import Annotated, Tuple
from typing import TYPE_CHECKING, Annotated, Tuple

import typer
from basic_memory.cli.app import claude_app
from basic_memory.cli.commands.command_utils import run_with_cleanup
from basic_memory.config import ConfigManager, get_project_config
from basic_memory.importers.claude_projects_importer import ClaudeProjectsImporter
from basic_memory.markdown import EntityParser, MarkdownProcessor
from basic_memory.services.file_service import FileService
from loguru import logger
from rich.console import Console
from rich.panel import Panel

if TYPE_CHECKING:
from basic_memory.markdown import MarkdownProcessor
from basic_memory.services.file_service import FileService

console = Console()


async def get_importer_dependencies() -> Tuple[MarkdownProcessor, FileService]:
"""Get MarkdownProcessor and FileService instances for importers."""
# Deferred: the markdown/file-service stack pulls SQLAlchemy and must load
# only when an import actually runs, not on every CLI start (#886).
from basic_memory.markdown import EntityParser, MarkdownProcessor
from basic_memory.services.file_service import FileService

config = get_project_config()
app_config = ConfigManager().config
entity_parser = EntityParser(config.home)
Expand Down Expand Up @@ -56,6 +65,9 @@ def import_projects(
markdown_processor, file_service = run_with_cleanup(get_importer_dependencies())

# Create the importer
# Deferred: importer stack loads at import-command run time only (#886).
from basic_memory.importers.claude_projects_importer import ClaudeProjectsImporter

importer = ClaudeProjectsImporter(
config.home, markdown_processor, file_service, project_name=config.name
)
Expand Down
20 changes: 16 additions & 4 deletions src/basic_memory/cli/commands/import_memory_json.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
"""Import command for basic-memory CLI to import from JSON memory format."""

# PEP 563 lazy annotations keep heavy importer types out of module import (#886).
from __future__ import annotations

import json
from pathlib import Path
from typing import Annotated, Tuple
from typing import TYPE_CHECKING, Annotated, Tuple

import typer
from basic_memory.cli.app import import_app
from basic_memory.cli.commands.command_utils import run_with_cleanup
from basic_memory.config import ConfigManager, get_project_config
from basic_memory.importers.memory_json_importer import MemoryJsonImporter
from basic_memory.markdown import EntityParser, MarkdownProcessor
from basic_memory.services.file_service import FileService
from loguru import logger
from rich.console import Console
from rich.panel import Panel

if TYPE_CHECKING:
from basic_memory.markdown import MarkdownProcessor
from basic_memory.services.file_service import FileService

console = Console()


async def get_importer_dependencies() -> Tuple[MarkdownProcessor, FileService]:
"""Get MarkdownProcessor and FileService instances for importers."""
# Deferred: the markdown/file-service stack pulls SQLAlchemy and must load
# only when an import actually runs, not on every CLI start (#886).
from basic_memory.markdown import EntityParser, MarkdownProcessor
from basic_memory.services.file_service import FileService

config = get_project_config()
app_config = ConfigManager().config
entity_parser = EntityParser(config.home)
Expand Down Expand Up @@ -55,6 +64,9 @@ def memory_json(
markdown_processor, file_service = run_with_cleanup(get_importer_dependencies())

# Create the importer
# Deferred: importer stack loads at import-command run time only (#886).
from basic_memory.importers.memory_json_importer import MemoryJsonImporter

importer = MemoryJsonImporter(
config.home, markdown_processor, file_service, project_name=config.name
)
Expand Down
Loading
Loading