mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
fix(cli): defer FastAPI and app imports out of CLI startup
Every basic-memory CLI invocation paid roughly 2 seconds of module-import cost before any work started, which blew the Claude Code plugin's SessionStart hook budget on cold machines (#886). The cost came from module-level imports that pulled the entire server stack into CLI startup: - mcp/async_client.py imported FastAPI at module level, so every consumer of get_client() loaded FastAPI even for cloud-routed or help-only paths. - mcp/clients/*.py imported call_* helpers from basic_memory.mcp.tools.utils, which executes the whole tools package __init__ — every MCP tool module plus fastmcp and the mcp SDK. - mcp/project_context.py imported fastmcp.Context and ToolError eagerly. - CLI command modules (tool, ci, schema) imported MCP tool functions at module level; db and the import_* commands pulled SQLAlchemy/Alembic and the markdown/file-service stack; status/doctor/orphans/command_utils imported ToolError (the mcp SDK) and basic_memory.db. - schemas/base.py imported dateparser (~0.13s) for one helper function. The fix only defers imports to the point of use (no behavior changes): FastAPI now loads inside _resolve_local_asgi_database alongside the existing lazy api.app import, so it is only paid when a request actually routes through the in-process ASGI transport; the typed clients import call_* per method; project_context uses PEP 563 annotations with Context under TYPE_CHECKING; the CLI command modules import their heavy dependencies inside the command bodies. Tests that patched the old module-level aliases now patch the source modules instead. Measured on a warm cache (python -X importtime / wall time): - import basic_memory.cli.main: 1.92s -> 0.45s - bm --help: 2.40s -> 0.52s - bm tool search-notes --help: 2.40s -> 0.86s A regression test asserts that importing the CLI entry module with full command registration leaves fastapi, sqlalchemy, alembic, fastmcp, mcp, basic_memory.api.app, basic_memory.db, basic_memory.markdown, basic_memory.mcp.tools, and basic_memory.services out of sys.modules. Fixes #886 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
committed by
Paul Hernandez
parent
253e240d68
commit
0247ef0ead
@@ -34,8 +34,10 @@ from basic_memory.ci.project_updates import (
|
||||
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()
|
||||
@@ -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():
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -7,16 +7,12 @@ import uuid
|
||||
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
|
||||
@@ -29,6 +25,12 @@ console = Console()
|
||||
|
||||
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]}"
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import Annotated, Optional
|
||||
|
||||
import typer
|
||||
from loguru import logger
|
||||
from mcp.server.fastmcp.exceptions import ToolError
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
@@ -50,6 +49,9 @@ def orphans(
|
||||
"""
|
||||
from basic_memory.cli.commands.command_utils import run_with_cleanup
|
||||
|
||||
# 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)
|
||||
with force_routing(local=local, cloud=cloud):
|
||||
|
||||
@@ -20,9 +20,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.config import ConfigManager
|
||||
from basic_memory.mcp.tools import schema_diff as mcp_schema_diff
|
||||
from basic_memory.mcp.tools import schema_infer as mcp_schema_infer
|
||||
from basic_memory.mcp.tools import schema_validate as mcp_schema_validate
|
||||
|
||||
# MCP tool functions are imported inside each command: 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()
|
||||
|
||||
@@ -189,6 +190,9 @@ def validate(
|
||||
Use --local to force local routing when cloud mode is enabled.
|
||||
Use --cloud to force cloud routing when cloud mode is disabled.
|
||||
"""
|
||||
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
|
||||
from basic_memory.mcp.tools import schema_validate as mcp_schema_validate
|
||||
|
||||
try:
|
||||
validate_routing_flags(local, cloud)
|
||||
project_name = _resolve_project_name(project)
|
||||
@@ -272,6 +276,9 @@ def infer(
|
||||
Use --local to force local routing when cloud mode is enabled.
|
||||
Use --cloud to force cloud routing when cloud mode is disabled.
|
||||
"""
|
||||
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
|
||||
from basic_memory.mcp.tools import schema_infer as mcp_schema_infer
|
||||
|
||||
try:
|
||||
validate_routing_flags(local, cloud)
|
||||
project_name = _resolve_project_name(project)
|
||||
@@ -352,6 +359,9 @@ def diff(
|
||||
Use --local to force local routing when cloud mode is enabled.
|
||||
Use --cloud to force cloud routing when cloud mode is disabled.
|
||||
"""
|
||||
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
|
||||
from basic_memory.mcp.tools import schema_diff as mcp_schema_diff
|
||||
|
||||
try:
|
||||
validate_routing_flags(local, cloud)
|
||||
project_name = _resolve_project_name(project)
|
||||
|
||||
@@ -5,7 +5,6 @@ import json
|
||||
import time
|
||||
from typing import Annotated, Dict, Optional, Set
|
||||
|
||||
from mcp.server.fastmcp.exceptions import ToolError
|
||||
import typer
|
||||
from loguru import logger
|
||||
from rich.console import Console
|
||||
@@ -231,6 +230,9 @@ def status(
|
||||
"""
|
||||
from basic_memory.cli.commands.command_utils import run_with_cleanup
|
||||
|
||||
# Deferred: ToolError lives in the mcp SDK, which must not load at CLI startup (#886).
|
||||
from mcp.server.fastmcp.exceptions import ToolError
|
||||
|
||||
# Trigger: --wait with a negative --timeout
|
||||
# Why: a negative deadline times out on the very first poll, producing a confusing
|
||||
# "Timed out after -5s" message instead of flagging the bad input. Raised
|
||||
|
||||
@@ -14,18 +14,10 @@ from loguru import logger
|
||||
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 build_context as mcp_build_context
|
||||
from basic_memory.mcp.tools import delete_note as mcp_delete_note
|
||||
from basic_memory.mcp.tools import edit_note as mcp_edit_note
|
||||
from basic_memory.mcp.tools import list_memory_projects as mcp_list_projects
|
||||
from basic_memory.mcp.tools import list_workspaces as mcp_list_workspaces
|
||||
from basic_memory.mcp.tools import read_note as mcp_read_note
|
||||
from basic_memory.mcp.tools import recent_activity as mcp_recent_activity
|
||||
from basic_memory.mcp.tools import schema_diff as mcp_schema_diff
|
||||
from basic_memory.mcp.tools import schema_infer as mcp_schema_infer
|
||||
from basic_memory.mcp.tools import schema_validate as mcp_schema_validate
|
||||
from basic_memory.mcp.tools import search_notes as mcp_search
|
||||
from basic_memory.mcp.tools import write_note as mcp_write_note
|
||||
|
||||
# MCP tool functions are imported inside each command: importing
|
||||
# basic_memory.mcp.tools loads the entire tool stack (fastmcp, mcp SDK,
|
||||
# SQLAlchemy), which would slow every CLI invocation, including --help (#886).
|
||||
|
||||
tool_app = typer.Typer()
|
||||
app.add_typer(tool_app, name="tool", help="Access to MCP tools via CLI")
|
||||
@@ -120,6 +112,9 @@ def write_note(
|
||||
bm tool write-note --title "My Note" --folder "notes" --overwrite
|
||||
bm tool write-note --title "My Note" --folder "notes" --local
|
||||
"""
|
||||
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
|
||||
from basic_memory.mcp.tools import write_note as mcp_write_note
|
||||
|
||||
try:
|
||||
validate_routing_flags(local, cloud)
|
||||
|
||||
@@ -206,6 +201,9 @@ def read_note(
|
||||
bm tool read-note my-note
|
||||
bm tool read-note my-note --include-frontmatter
|
||||
"""
|
||||
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
|
||||
from basic_memory.mcp.tools import read_note as mcp_read_note
|
||||
|
||||
try:
|
||||
validate_routing_flags(local, cloud)
|
||||
|
||||
@@ -272,6 +270,9 @@ def delete_note(
|
||||
bm tool delete-note notes/old-draft
|
||||
bm tool delete-note docs/archive --is-directory
|
||||
"""
|
||||
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
|
||||
from basic_memory.mcp.tools import delete_note as mcp_delete_note
|
||||
|
||||
try:
|
||||
validate_routing_flags(local, cloud)
|
||||
|
||||
@@ -346,6 +347,9 @@ def edit_note(
|
||||
bm tool edit-note my-note --operation find_replace --find-text "old" --content "new"
|
||||
bm tool edit-note my-note --operation replace_section --section "## Notes" --content "updated"
|
||||
"""
|
||||
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
|
||||
from basic_memory.mcp.tools import edit_note as mcp_edit_note
|
||||
|
||||
try:
|
||||
validate_routing_flags(local, cloud)
|
||||
|
||||
@@ -413,6 +417,9 @@ def build_context(
|
||||
bm tool build-context memory://specs/search
|
||||
bm tool build-context specs/search --depth 2 --timeframe 30d
|
||||
"""
|
||||
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
|
||||
from basic_memory.mcp.tools import build_context as mcp_build_context
|
||||
|
||||
try:
|
||||
validate_routing_flags(local, cloud)
|
||||
|
||||
@@ -476,6 +483,9 @@ def recent_activity(
|
||||
bm tool recent-activity --timeframe 30d --page-size 20
|
||||
bm tool recent-activity --type entity --type observation
|
||||
"""
|
||||
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
|
||||
from basic_memory.mcp.tools import recent_activity as mcp_recent_activity
|
||||
|
||||
try:
|
||||
validate_routing_flags(local, cloud)
|
||||
|
||||
@@ -582,6 +592,9 @@ def search_notes(
|
||||
bm tool search-notes --meta status=draft
|
||||
bm tool search-notes "auth" --entity-type observation --category requirement
|
||||
"""
|
||||
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
|
||||
from basic_memory.mcp.tools import search_notes as mcp_search
|
||||
|
||||
try:
|
||||
validate_routing_flags(local, cloud)
|
||||
|
||||
@@ -687,6 +700,9 @@ def list_projects(
|
||||
bm tool list-projects
|
||||
bm tool list-projects --local
|
||||
"""
|
||||
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
|
||||
from basic_memory.mcp.tools import list_memory_projects as mcp_list_projects
|
||||
|
||||
try:
|
||||
validate_routing_flags(local, cloud)
|
||||
|
||||
@@ -720,6 +736,9 @@ def list_workspaces(
|
||||
bm tool list-workspaces
|
||||
bm tool list-workspaces --cloud
|
||||
"""
|
||||
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
|
||||
from basic_memory.mcp.tools import list_workspaces as mcp_list_workspaces
|
||||
|
||||
try:
|
||||
validate_routing_flags(local, cloud)
|
||||
|
||||
@@ -772,6 +791,9 @@ def schema_validate(
|
||||
bm tool schema-validate people/ada-lovelace.md
|
||||
bm tool schema-validate --project research
|
||||
"""
|
||||
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
|
||||
from basic_memory.mcp.tools import schema_validate as mcp_schema_validate
|
||||
|
||||
try:
|
||||
validate_routing_flags(local, cloud)
|
||||
|
||||
@@ -840,6 +862,9 @@ def schema_infer(
|
||||
bm tool schema-infer meeting --threshold 0.5
|
||||
bm tool schema-infer person --project research
|
||||
"""
|
||||
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
|
||||
from basic_memory.mcp.tools import schema_infer as mcp_schema_infer
|
||||
|
||||
try:
|
||||
validate_routing_flags(local, cloud)
|
||||
|
||||
@@ -896,6 +921,9 @@ def schema_diff(
|
||||
bm tool schema-diff person
|
||||
bm tool schema-diff person --project research
|
||||
"""
|
||||
# Deferred: loading the MCP tool stack at module import slows CLI startup (#886).
|
||||
from basic_memory.mcp.tools import schema_diff as mcp_schema_diff
|
||||
|
||||
try:
|
||||
validate_routing_flags(local, cloud)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user