basic-memory 0.7.0__py3-none-any.whl → 0.17.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of basic-memory might be problematic. Click here for more details.
- basic_memory/__init__.py +5 -1
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +130 -20
- basic_memory/alembic/migrations.py +4 -9
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
- basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
- basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
- basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
- basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
- basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
- basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
- basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
- basic_memory/api/app.py +87 -20
- basic_memory/api/container.py +133 -0
- basic_memory/api/routers/__init__.py +4 -1
- basic_memory/api/routers/directory_router.py +84 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +180 -23
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +9 -64
- basic_memory/api/routers/project_router.py +460 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +136 -11
- basic_memory/api/routers/search_router.py +5 -5
- basic_memory/api/routers/utils.py +169 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/api/v2/__init__.py +35 -0
- basic_memory/api/v2/routers/__init__.py +21 -0
- basic_memory/api/v2/routers/directory_router.py +93 -0
- basic_memory/api/v2/routers/importer_router.py +181 -0
- basic_memory/api/v2/routers/knowledge_router.py +427 -0
- basic_memory/api/v2/routers/memory_router.py +130 -0
- basic_memory/api/v2/routers/project_router.py +359 -0
- basic_memory/api/v2/routers/prompt_router.py +269 -0
- basic_memory/api/v2/routers/resource_router.py +286 -0
- basic_memory/api/v2/routers/search_router.py +73 -0
- basic_memory/cli/app.py +80 -10
- basic_memory/cli/auth.py +300 -0
- basic_memory/cli/commands/__init__.py +15 -2
- basic_memory/cli/commands/cloud/__init__.py +6 -0
- basic_memory/cli/commands/cloud/api_client.py +127 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
- basic_memory/cli/commands/cloud/cloud_utils.py +108 -0
- basic_memory/cli/commands/cloud/core_commands.py +195 -0
- basic_memory/cli/commands/cloud/rclone_commands.py +397 -0
- basic_memory/cli/commands/cloud/rclone_config.py +110 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
- basic_memory/cli/commands/cloud/upload.py +240 -0
- basic_memory/cli/commands/cloud/upload_command.py +124 -0
- basic_memory/cli/commands/command_utils.py +99 -0
- basic_memory/cli/commands/db.py +87 -12
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +47 -223
- basic_memory/cli/commands/import_claude_conversations.py +48 -171
- basic_memory/cli/commands/import_claude_projects.py +53 -160
- basic_memory/cli/commands/import_memory_json.py +55 -111
- basic_memory/cli/commands/mcp.py +67 -11
- basic_memory/cli/commands/project.py +889 -0
- basic_memory/cli/commands/status.py +52 -34
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/container.py +84 -0
- basic_memory/cli/main.py +14 -6
- basic_memory/config.py +580 -26
- basic_memory/db.py +285 -28
- basic_memory/deps/__init__.py +293 -0
- basic_memory/deps/config.py +26 -0
- basic_memory/deps/db.py +56 -0
- basic_memory/deps/importers.py +200 -0
- basic_memory/deps/projects.py +238 -0
- basic_memory/deps/repositories.py +179 -0
- basic_memory/deps/services.py +480 -0
- basic_memory/deps.py +16 -185
- basic_memory/file_utils.py +318 -54
- basic_memory/ignore_utils.py +297 -0
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +100 -0
- basic_memory/importers/chatgpt_importer.py +245 -0
- basic_memory/importers/claude_conversations_importer.py +192 -0
- basic_memory/importers/claude_projects_importer.py +184 -0
- basic_memory/importers/memory_json_importer.py +128 -0
- basic_memory/importers/utils.py +61 -0
- basic_memory/markdown/entity_parser.py +182 -23
- basic_memory/markdown/markdown_processor.py +70 -7
- basic_memory/markdown/plugins.py +43 -23
- basic_memory/markdown/schemas.py +1 -1
- basic_memory/markdown/utils.py +38 -14
- basic_memory/mcp/async_client.py +135 -4
- basic_memory/mcp/clients/__init__.py +28 -0
- basic_memory/mcp/clients/directory.py +70 -0
- basic_memory/mcp/clients/knowledge.py +176 -0
- basic_memory/mcp/clients/memory.py +120 -0
- basic_memory/mcp/clients/project.py +89 -0
- basic_memory/mcp/clients/resource.py +71 -0
- basic_memory/mcp/clients/search.py +65 -0
- basic_memory/mcp/container.py +110 -0
- basic_memory/mcp/project_context.py +155 -0
- basic_memory/mcp/prompts/__init__.py +19 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
- basic_memory/mcp/prompts/continue_conversation.py +62 -0
- basic_memory/mcp/prompts/recent_activity.py +188 -0
- basic_memory/mcp/prompts/search.py +57 -0
- basic_memory/mcp/prompts/utils.py +162 -0
- basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
- basic_memory/mcp/resources/project_info.py +71 -0
- basic_memory/mcp/server.py +61 -9
- basic_memory/mcp/tools/__init__.py +33 -21
- basic_memory/mcp/tools/build_context.py +120 -0
- basic_memory/mcp/tools/canvas.py +152 -0
- basic_memory/mcp/tools/chatgpt_tools.py +190 -0
- basic_memory/mcp/tools/delete_note.py +249 -0
- basic_memory/mcp/tools/edit_note.py +325 -0
- basic_memory/mcp/tools/list_directory.py +157 -0
- basic_memory/mcp/tools/move_note.py +549 -0
- basic_memory/mcp/tools/project_management.py +204 -0
- basic_memory/mcp/tools/read_content.py +281 -0
- basic_memory/mcp/tools/read_note.py +265 -0
- basic_memory/mcp/tools/recent_activity.py +528 -0
- basic_memory/mcp/tools/search.py +377 -24
- basic_memory/mcp/tools/utils.py +402 -16
- basic_memory/mcp/tools/view_note.py +78 -0
- basic_memory/mcp/tools/write_note.py +230 -0
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +82 -17
- basic_memory/models/project.py +93 -0
- basic_memory/models/search.py +68 -8
- basic_memory/project_resolver.py +222 -0
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +437 -8
- basic_memory/repository/observation_repository.py +36 -3
- basic_memory/repository/postgres_search_repository.py +451 -0
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +140 -0
- basic_memory/repository/relation_repository.py +79 -4
- basic_memory/repository/repository.py +148 -29
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +79 -268
- basic_memory/repository/search_repository_base.py +241 -0
- basic_memory/repository/sqlite_search_repository.py +437 -0
- basic_memory/runtime.py +61 -0
- basic_memory/schemas/__init__.py +22 -9
- basic_memory/schemas/base.py +131 -12
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/directory.py +31 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +194 -25
- basic_memory/schemas/project_info.py +213 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +56 -2
- basic_memory/schemas/response.py +85 -28
- basic_memory/schemas/search.py +36 -35
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/schemas/v2/__init__.py +27 -0
- basic_memory/schemas/v2/entity.py +133 -0
- basic_memory/schemas/v2/resource.py +47 -0
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +451 -138
- basic_memory/services/directory_service.py +310 -0
- basic_memory/services/entity_service.py +636 -71
- basic_memory/services/exceptions.py +21 -0
- basic_memory/services/file_service.py +402 -33
- basic_memory/services/initialization.py +216 -0
- basic_memory/services/link_resolver.py +50 -56
- basic_memory/services/project_service.py +888 -0
- basic_memory/services/search_service.py +232 -37
- basic_memory/sync/__init__.py +4 -2
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/coordinator.py +160 -0
- basic_memory/sync/sync_service.py +1200 -109
- basic_memory/sync/watch_service.py +432 -135
- basic_memory/telemetry.py +249 -0
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +407 -54
- basic_memory-0.17.4.dist-info/METADATA +617 -0
- basic_memory-0.17.4.dist-info/RECORD +193 -0
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +1 -0
- basic_memory/alembic/README +0 -1
- basic_memory/cli/commands/sync.py +0 -206
- basic_memory/cli/commands/tools.py +0 -157
- basic_memory/mcp/tools/knowledge.py +0 -68
- basic_memory/mcp/tools/memory.py +0 -170
- basic_memory/mcp/tools/notes.py +0 -202
- basic_memory/schemas/discovery.py +0 -28
- basic_memory/sync/file_change_scanner.py +0 -158
- basic_memory/sync/utils.py +0 -31
- basic_memory-0.7.0.dist-info/METADATA +0 -378
- basic_memory-0.7.0.dist-info/RECORD +0 -82
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Typed client for search API operations.
|
|
2
|
+
|
|
3
|
+
Encapsulates all /v2/projects/{project_id}/search/* endpoints.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from httpx import AsyncClient
|
|
9
|
+
|
|
10
|
+
from basic_memory.mcp.tools.utils import call_post
|
|
11
|
+
from basic_memory.schemas.search import SearchResponse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SearchClient:
|
|
15
|
+
"""Typed client for search operations.
|
|
16
|
+
|
|
17
|
+
Centralizes:
|
|
18
|
+
- API path construction for /v2/projects/{project_id}/search/*
|
|
19
|
+
- Response validation via Pydantic models
|
|
20
|
+
- Consistent error handling through call_* utilities
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
async with get_client() as http_client:
|
|
24
|
+
client = SearchClient(http_client, project_id)
|
|
25
|
+
results = await client.search(search_query.model_dump())
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, http_client: AsyncClient, project_id: str):
|
|
29
|
+
"""Initialize the search client.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
http_client: HTTPX AsyncClient for making requests
|
|
33
|
+
project_id: Project external_id (UUID) for API calls
|
|
34
|
+
"""
|
|
35
|
+
self.http_client = http_client
|
|
36
|
+
self.project_id = project_id
|
|
37
|
+
self._base_path = f"/v2/projects/{project_id}/search"
|
|
38
|
+
|
|
39
|
+
async def search(
|
|
40
|
+
self,
|
|
41
|
+
query: dict[str, Any],
|
|
42
|
+
*,
|
|
43
|
+
page: int = 1,
|
|
44
|
+
page_size: int = 10,
|
|
45
|
+
) -> SearchResponse:
|
|
46
|
+
"""Search across all content in the knowledge base.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
query: Search query dict (from SearchQuery.model_dump())
|
|
50
|
+
page: Page number (1-indexed)
|
|
51
|
+
page_size: Results per page
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
SearchResponse with results and pagination
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
ToolError: If the request fails
|
|
58
|
+
"""
|
|
59
|
+
response = await call_post(
|
|
60
|
+
self.http_client,
|
|
61
|
+
f"{self._base_path}/",
|
|
62
|
+
json=query,
|
|
63
|
+
params={"page": page, "page_size": page_size},
|
|
64
|
+
)
|
|
65
|
+
return SearchResponse.model_validate(response.json())
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""MCP composition root for Basic Memory.
|
|
2
|
+
|
|
3
|
+
This container owns reading ConfigManager and environment variables for the
|
|
4
|
+
MCP server entrypoint. Downstream modules receive config/dependencies explicitly
|
|
5
|
+
rather than reading globals.
|
|
6
|
+
|
|
7
|
+
Design principles:
|
|
8
|
+
- Only this module reads ConfigManager directly
|
|
9
|
+
- Runtime mode (cloud/local/test) is resolved here
|
|
10
|
+
- File sync decisions are centralized here
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from basic_memory.config import BasicMemoryConfig, ConfigManager
|
|
17
|
+
from basic_memory.runtime import RuntimeMode, resolve_runtime_mode
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
20
|
+
from basic_memory.sync import SyncCoordinator
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class McpContainer:
|
|
25
|
+
"""Composition root for the MCP server entrypoint.
|
|
26
|
+
|
|
27
|
+
Holds resolved configuration and runtime context.
|
|
28
|
+
Created once at server startup, then used to wire dependencies.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
config: BasicMemoryConfig
|
|
32
|
+
mode: RuntimeMode
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def create(cls) -> "McpContainer":
|
|
36
|
+
"""Create container by reading ConfigManager.
|
|
37
|
+
|
|
38
|
+
This is the single point where MCP reads global config.
|
|
39
|
+
"""
|
|
40
|
+
config = ConfigManager().config
|
|
41
|
+
mode = resolve_runtime_mode(
|
|
42
|
+
cloud_mode_enabled=config.cloud_mode_enabled,
|
|
43
|
+
is_test_env=config.is_test_env,
|
|
44
|
+
)
|
|
45
|
+
return cls(config=config, mode=mode)
|
|
46
|
+
|
|
47
|
+
# --- Runtime Mode Properties ---
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def should_sync_files(self) -> bool:
|
|
51
|
+
"""Whether local file sync should be started.
|
|
52
|
+
|
|
53
|
+
Sync is enabled when:
|
|
54
|
+
- sync_changes is True in config
|
|
55
|
+
- Not in test mode (tests manage their own sync)
|
|
56
|
+
- Not in cloud mode (cloud handles sync differently)
|
|
57
|
+
"""
|
|
58
|
+
return (
|
|
59
|
+
self.config.sync_changes and not self.mode.is_test and not self.mode.is_cloud
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def sync_skip_reason(self) -> str | None:
|
|
64
|
+
"""Reason why sync is skipped, or None if sync should run.
|
|
65
|
+
|
|
66
|
+
Useful for logging why sync was disabled.
|
|
67
|
+
"""
|
|
68
|
+
if self.mode.is_test:
|
|
69
|
+
return "Test environment detected"
|
|
70
|
+
if self.mode.is_cloud:
|
|
71
|
+
return "Cloud mode enabled"
|
|
72
|
+
if not self.config.sync_changes:
|
|
73
|
+
return "Sync changes disabled"
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def create_sync_coordinator(self) -> "SyncCoordinator":
|
|
77
|
+
"""Create a SyncCoordinator with this container's settings.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
SyncCoordinator configured for this runtime environment
|
|
81
|
+
"""
|
|
82
|
+
# Deferred import to avoid circular dependency
|
|
83
|
+
from basic_memory.sync import SyncCoordinator
|
|
84
|
+
|
|
85
|
+
return SyncCoordinator(
|
|
86
|
+
config=self.config,
|
|
87
|
+
should_sync=self.should_sync_files,
|
|
88
|
+
skip_reason=self.sync_skip_reason,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Module-level container instance (set by lifespan)
|
|
93
|
+
_container: McpContainer | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_container() -> McpContainer:
|
|
97
|
+
"""Get the current MCP container.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
RuntimeError: If container hasn't been initialized
|
|
101
|
+
"""
|
|
102
|
+
if _container is None:
|
|
103
|
+
raise RuntimeError("MCP container not initialized. Call set_container() first.")
|
|
104
|
+
return _container
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def set_container(container: McpContainer) -> None:
|
|
108
|
+
"""Set the MCP container (called by lifespan)."""
|
|
109
|
+
global _container
|
|
110
|
+
_container = container
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Project context utilities for Basic Memory MCP server.
|
|
2
|
+
|
|
3
|
+
Provides project lookup utilities for MCP tools.
|
|
4
|
+
Handles project validation and context management in one place.
|
|
5
|
+
|
|
6
|
+
Note: This module uses ProjectResolver for unified project resolution.
|
|
7
|
+
The resolve_project_parameter function is a thin wrapper for backwards
|
|
8
|
+
compatibility with existing MCP tools.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Optional, List
|
|
12
|
+
from httpx import AsyncClient
|
|
13
|
+
from httpx._types import (
|
|
14
|
+
HeaderTypes,
|
|
15
|
+
)
|
|
16
|
+
from loguru import logger
|
|
17
|
+
from fastmcp import Context
|
|
18
|
+
|
|
19
|
+
from basic_memory.config import ConfigManager
|
|
20
|
+
from basic_memory.project_resolver import ProjectResolver
|
|
21
|
+
from basic_memory.schemas.project_info import ProjectItem, ProjectList
|
|
22
|
+
from basic_memory.utils import generate_permalink
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def resolve_project_parameter(
|
|
26
|
+
project: Optional[str] = None,
|
|
27
|
+
allow_discovery: bool = False,
|
|
28
|
+
cloud_mode: Optional[bool] = None,
|
|
29
|
+
default_project_mode: Optional[bool] = None,
|
|
30
|
+
default_project: Optional[str] = None,
|
|
31
|
+
) -> Optional[str]:
|
|
32
|
+
"""Resolve project parameter using three-tier hierarchy.
|
|
33
|
+
|
|
34
|
+
This is a thin wrapper around ProjectResolver for backwards compatibility.
|
|
35
|
+
New code should consider using ProjectResolver directly for more detailed
|
|
36
|
+
resolution information.
|
|
37
|
+
|
|
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
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
project: Optional explicit project parameter
|
|
48
|
+
allow_discovery: If True, allows returning None in cloud mode for discovery mode
|
|
49
|
+
(used by tools like recent_activity that can operate across all projects)
|
|
50
|
+
cloud_mode: Optional explicit cloud mode. If not provided, reads from ConfigManager.
|
|
51
|
+
default_project_mode: Optional explicit default project mode. If not provided, reads from ConfigManager.
|
|
52
|
+
default_project: Optional explicit default project. If not provided, reads from ConfigManager.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Resolved project name or None if no resolution possible
|
|
56
|
+
"""
|
|
57
|
+
# Load config for any values not explicitly provided
|
|
58
|
+
if cloud_mode is None or default_project_mode is None or default_project is None:
|
|
59
|
+
config = ConfigManager().config
|
|
60
|
+
if cloud_mode is None:
|
|
61
|
+
cloud_mode = config.cloud_mode
|
|
62
|
+
if default_project_mode is None:
|
|
63
|
+
default_project_mode = config.default_project_mode
|
|
64
|
+
if default_project is None:
|
|
65
|
+
default_project = config.default_project
|
|
66
|
+
|
|
67
|
+
# Create resolver with configuration and resolve
|
|
68
|
+
resolver = ProjectResolver.from_env(
|
|
69
|
+
cloud_mode=cloud_mode,
|
|
70
|
+
default_project_mode=default_project_mode,
|
|
71
|
+
default_project=default_project,
|
|
72
|
+
)
|
|
73
|
+
result = resolver.resolve(project=project, allow_discovery=allow_discovery)
|
|
74
|
+
return result.project
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def get_project_names(client: AsyncClient, headers: HeaderTypes | None = None) -> List[str]:
|
|
78
|
+
# Deferred import to avoid circular dependency with tools
|
|
79
|
+
from basic_memory.mcp.tools.utils import call_get
|
|
80
|
+
|
|
81
|
+
response = await call_get(client, "/projects/projects", headers=headers)
|
|
82
|
+
project_list = ProjectList.model_validate(response.json())
|
|
83
|
+
return [project.name for project in project_list.projects]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def get_active_project(
|
|
87
|
+
client: AsyncClient,
|
|
88
|
+
project: Optional[str] = None,
|
|
89
|
+
context: Optional[Context] = None,
|
|
90
|
+
headers: HeaderTypes | None = None,
|
|
91
|
+
) -> ProjectItem:
|
|
92
|
+
"""Get and validate project, setting it in context if available.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
client: HTTP client for API calls
|
|
96
|
+
project: Optional project name (resolved using hierarchy)
|
|
97
|
+
context: Optional FastMCP context to cache the result
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
The validated project item
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ValueError: If no project can be resolved
|
|
104
|
+
HTTPError: If project doesn't exist or is inaccessible
|
|
105
|
+
"""
|
|
106
|
+
# Deferred import to avoid circular dependency with tools
|
|
107
|
+
from basic_memory.mcp.tools.utils import call_get
|
|
108
|
+
|
|
109
|
+
resolved_project = await resolve_project_parameter(project)
|
|
110
|
+
if not resolved_project:
|
|
111
|
+
project_names = await get_project_names(client, headers)
|
|
112
|
+
raise ValueError(
|
|
113
|
+
"No project specified. "
|
|
114
|
+
"Either set 'default_project_mode=true' in config, or use 'project' argument.\n"
|
|
115
|
+
f"Available projects: {project_names}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
project = resolved_project
|
|
119
|
+
|
|
120
|
+
# Check if already cached in context
|
|
121
|
+
if context:
|
|
122
|
+
cached_project = context.get_state("active_project")
|
|
123
|
+
if cached_project and cached_project.name == project:
|
|
124
|
+
logger.debug(f"Using cached project from context: {project}")
|
|
125
|
+
return cached_project
|
|
126
|
+
|
|
127
|
+
# Validate project exists by calling API
|
|
128
|
+
logger.debug(f"Validating project: {project}")
|
|
129
|
+
permalink = generate_permalink(project)
|
|
130
|
+
response = await call_get(client, f"/{permalink}/project/item", headers=headers)
|
|
131
|
+
active_project = ProjectItem.model_validate(response.json())
|
|
132
|
+
|
|
133
|
+
# Cache in context if available
|
|
134
|
+
if context:
|
|
135
|
+
context.set_state("active_project", active_project)
|
|
136
|
+
logger.debug(f"Cached project in context: {project}")
|
|
137
|
+
|
|
138
|
+
logger.debug(f"Validated project: {active_project.name}")
|
|
139
|
+
return active_project
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def add_project_metadata(result: str, project_name: str) -> str:
|
|
143
|
+
"""Add project context as metadata footer for assistant session tracking.
|
|
144
|
+
|
|
145
|
+
Provides clear project context to help the assistant remember which
|
|
146
|
+
project is being used throughout the conversation session.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
result: The tool result string
|
|
150
|
+
project_name: The project name that was used
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Result with project session tracking metadata
|
|
154
|
+
"""
|
|
155
|
+
return f"{result}\n\n[Session: Using project '{project_name}']"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Basic Memory MCP prompts.
|
|
2
|
+
|
|
3
|
+
Prompts are a special type of tool that returns a string response
|
|
4
|
+
formatted for a user to read, typically invoking one or more tools
|
|
5
|
+
and transforming their results into user-friendly text.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Import individual prompt modules to register them with the MCP server
|
|
9
|
+
from basic_memory.mcp.prompts import continue_conversation
|
|
10
|
+
from basic_memory.mcp.prompts import recent_activity
|
|
11
|
+
from basic_memory.mcp.prompts import search
|
|
12
|
+
from basic_memory.mcp.prompts import ai_assistant_guide
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ai_assistant_guide",
|
|
16
|
+
"continue_conversation",
|
|
17
|
+
"recent_activity",
|
|
18
|
+
"search",
|
|
19
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from basic_memory.config import ConfigManager
|
|
4
|
+
from basic_memory.mcp.server import mcp
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@mcp.resource(
|
|
9
|
+
uri="memory://ai_assistant_guide",
|
|
10
|
+
name="ai assistant guide",
|
|
11
|
+
description="Give an AI assistant guidance on how to use Basic Memory tools effectively",
|
|
12
|
+
)
|
|
13
|
+
def ai_assistant_guide() -> str:
|
|
14
|
+
"""Return a concise guide on Basic Memory tools and how to use them.
|
|
15
|
+
|
|
16
|
+
Dynamically adapts instructions based on configuration:
|
|
17
|
+
- Default project mode: Simplified instructions with automatic project
|
|
18
|
+
- Regular mode: Project discovery and selection guidance
|
|
19
|
+
- CLI constraint mode: Single project constraint information
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
A focused guide on Basic Memory usage.
|
|
23
|
+
"""
|
|
24
|
+
logger.info("Loading AI assistant guide resource")
|
|
25
|
+
|
|
26
|
+
# Load base guide content
|
|
27
|
+
guide_doc = Path(__file__).parent.parent / "resources" / "ai_assistant_guide.md"
|
|
28
|
+
content = guide_doc.read_text(encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
# Check configuration for mode-specific instructions
|
|
31
|
+
config = ConfigManager().config
|
|
32
|
+
|
|
33
|
+
# Add mode-specific header
|
|
34
|
+
mode_info = ""
|
|
35
|
+
if config.default_project_mode: # pragma: no cover
|
|
36
|
+
mode_info = f"""
|
|
37
|
+
# 🎯 Default Project Mode Active
|
|
38
|
+
|
|
39
|
+
**Current Configuration**: All operations automatically use project '{config.default_project}'
|
|
40
|
+
|
|
41
|
+
**Simplified Usage**: You don't need to specify the project parameter in tool calls.
|
|
42
|
+
- `write_note(title="Note", content="...", folder="docs")` ✅
|
|
43
|
+
- Project parameter is optional and will default to '{config.default_project}'
|
|
44
|
+
- To use a different project, explicitly specify: `project="other-project"`
|
|
45
|
+
|
|
46
|
+
────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
else: # pragma: no cover
|
|
50
|
+
mode_info = """
|
|
51
|
+
# 🔧 Multi-Project Mode Active
|
|
52
|
+
|
|
53
|
+
**Current Configuration**: Project parameter required for all operations
|
|
54
|
+
|
|
55
|
+
**Project Discovery Required**: Use these tools to select a project:
|
|
56
|
+
- `list_memory_projects()` - See all available projects
|
|
57
|
+
- `recent_activity()` - Get project activity and recommendations
|
|
58
|
+
- Remember the user's project choice throughout the conversation
|
|
59
|
+
|
|
60
|
+
────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
# Prepend mode info to the guide
|
|
65
|
+
enhanced_content = mode_info + content
|
|
66
|
+
|
|
67
|
+
logger.info(
|
|
68
|
+
f"Loaded AI assistant guide ({len(enhanced_content)} chars) with mode: {'default_project' if config.default_project_mode else 'multi_project'}"
|
|
69
|
+
)
|
|
70
|
+
return enhanced_content
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Session continuation prompts for Basic Memory MCP server.
|
|
2
|
+
|
|
3
|
+
These prompts help users continue conversations and work across sessions,
|
|
4
|
+
providing context from previous interactions to maintain continuity.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Annotated, Optional
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
from basic_memory.config import get_project_config
|
|
13
|
+
from basic_memory.mcp.async_client import get_client
|
|
14
|
+
from basic_memory.mcp.server import mcp
|
|
15
|
+
from basic_memory.mcp.tools.utils import call_post
|
|
16
|
+
from basic_memory.schemas.base import TimeFrame
|
|
17
|
+
from basic_memory.schemas.prompt import ContinueConversationRequest
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@mcp.prompt(
|
|
21
|
+
name="continue_conversation",
|
|
22
|
+
description="Continue a previous conversation",
|
|
23
|
+
)
|
|
24
|
+
async def continue_conversation(
|
|
25
|
+
topic: Annotated[Optional[str], Field(description="Topic or keyword to search for")] = None,
|
|
26
|
+
timeframe: Annotated[
|
|
27
|
+
Optional[TimeFrame],
|
|
28
|
+
Field(description="How far back to look for activity (e.g. '1d', '1 week')"),
|
|
29
|
+
] = None,
|
|
30
|
+
) -> str:
|
|
31
|
+
"""Continue a previous conversation or work session.
|
|
32
|
+
|
|
33
|
+
This prompt helps you pick up where you left off by finding recent context
|
|
34
|
+
about a specific topic or showing general recent activity.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
topic: Topic or keyword to search for (optional)
|
|
38
|
+
timeframe: How far back to look for activity
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Context from previous sessions on this topic
|
|
42
|
+
"""
|
|
43
|
+
logger.info(f"Continuing session, topic: {topic}, timeframe: {timeframe}")
|
|
44
|
+
|
|
45
|
+
async with get_client() as client:
|
|
46
|
+
# Create request model
|
|
47
|
+
request = ContinueConversationRequest( # pyright: ignore [reportCallIssue]
|
|
48
|
+
topic=topic, timeframe=timeframe
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
project_url = get_project_config().project_url
|
|
52
|
+
|
|
53
|
+
# Call the prompt API endpoint
|
|
54
|
+
response = await call_post(
|
|
55
|
+
client,
|
|
56
|
+
f"{project_url}/prompt/continue-conversation",
|
|
57
|
+
json=request.model_dump(exclude_none=True),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Extract the rendered prompt from the response
|
|
61
|
+
result = response.json()
|
|
62
|
+
return result["prompt"]
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Recent activity prompts for Basic Memory MCP server.
|
|
2
|
+
|
|
3
|
+
These prompts help users see what has changed in their knowledge base recently.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Annotated, Optional
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from pydantic import Field
|
|
10
|
+
|
|
11
|
+
from basic_memory.mcp.prompts.utils import format_prompt_context, PromptContext, PromptContextItem
|
|
12
|
+
from basic_memory.mcp.server import mcp
|
|
13
|
+
from basic_memory.mcp.tools.recent_activity import recent_activity
|
|
14
|
+
from basic_memory.schemas.base import TimeFrame
|
|
15
|
+
from basic_memory.schemas.memory import GraphContext, ProjectActivitySummary
|
|
16
|
+
from basic_memory.schemas.search import SearchItemType
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@mcp.prompt(
|
|
20
|
+
name="recent_activity",
|
|
21
|
+
description="Get recent activity from a specific project or across all projects",
|
|
22
|
+
)
|
|
23
|
+
async def recent_activity_prompt(
|
|
24
|
+
timeframe: Annotated[
|
|
25
|
+
TimeFrame,
|
|
26
|
+
Field(description="How far back to look for activity (e.g. '1d', '1 week')"),
|
|
27
|
+
] = "7d",
|
|
28
|
+
project: Annotated[
|
|
29
|
+
Optional[str],
|
|
30
|
+
Field(
|
|
31
|
+
description="Specific project to get activity from (None for discovery across all projects)"
|
|
32
|
+
),
|
|
33
|
+
] = None,
|
|
34
|
+
) -> str:
|
|
35
|
+
"""Get recent activity from a specific project or across all projects.
|
|
36
|
+
|
|
37
|
+
This prompt helps you see what's changed recently in the knowledge base.
|
|
38
|
+
In discovery mode (project=None), it shows activity across all projects.
|
|
39
|
+
In project-specific mode, it shows detailed activity for one project.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
timeframe: How far back to look for activity (e.g. '1d', '1 week')
|
|
43
|
+
project: Specific project to get activity from (None for discovery across all projects)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Formatted summary of recent activity
|
|
47
|
+
"""
|
|
48
|
+
logger.info(f"Getting recent activity, timeframe: {timeframe}, project: {project}")
|
|
49
|
+
|
|
50
|
+
recent = await recent_activity.fn(
|
|
51
|
+
project=project, timeframe=timeframe, type=[SearchItemType.ENTITY]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Extract primary results from the hierarchical structure
|
|
55
|
+
primary_results = []
|
|
56
|
+
related_results = []
|
|
57
|
+
|
|
58
|
+
if isinstance(recent, ProjectActivitySummary):
|
|
59
|
+
# Discovery mode - extract results from all projects
|
|
60
|
+
for _, project_activity in recent.projects.items():
|
|
61
|
+
if project_activity.activity.results:
|
|
62
|
+
# Take up to 2 primary results per project
|
|
63
|
+
for item in project_activity.activity.results[:2]:
|
|
64
|
+
primary_results.append(item.primary_result)
|
|
65
|
+
# Add up to 1 related result per primary item
|
|
66
|
+
if item.related_results:
|
|
67
|
+
related_results.extend(item.related_results[:1]) # pragma: no cover
|
|
68
|
+
|
|
69
|
+
# Limit total results for readability
|
|
70
|
+
primary_results = primary_results[:8]
|
|
71
|
+
related_results = related_results[:6]
|
|
72
|
+
|
|
73
|
+
elif isinstance(recent, GraphContext):
|
|
74
|
+
# Project-specific mode - use existing logic
|
|
75
|
+
if recent.results:
|
|
76
|
+
# Take up to 5 primary results
|
|
77
|
+
for item in recent.results[:5]:
|
|
78
|
+
primary_results.append(item.primary_result)
|
|
79
|
+
# Add up to 2 related results per primary item
|
|
80
|
+
if item.related_results:
|
|
81
|
+
related_results.extend(item.related_results[:2]) # pragma: no cover
|
|
82
|
+
|
|
83
|
+
# Set topic based on mode
|
|
84
|
+
if project:
|
|
85
|
+
topic = f"Recent Activity in {project} ({timeframe})"
|
|
86
|
+
else:
|
|
87
|
+
topic = f"Recent Activity Across All Projects ({timeframe})"
|
|
88
|
+
|
|
89
|
+
prompt_context = format_prompt_context(
|
|
90
|
+
PromptContext(
|
|
91
|
+
topic=topic,
|
|
92
|
+
timeframe=timeframe,
|
|
93
|
+
results=[
|
|
94
|
+
PromptContextItem(
|
|
95
|
+
primary_results=primary_results,
|
|
96
|
+
related_results=related_results[:10], # Limit total related results
|
|
97
|
+
)
|
|
98
|
+
],
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Add mode-specific suggestions
|
|
103
|
+
first_title = "Recent Topic"
|
|
104
|
+
if primary_results and len(primary_results) > 0:
|
|
105
|
+
first_title = primary_results[0].title
|
|
106
|
+
|
|
107
|
+
if project:
|
|
108
|
+
# Project-specific suggestions
|
|
109
|
+
capture_suggestions = f"""
|
|
110
|
+
## Opportunity to Capture Activity Summary
|
|
111
|
+
|
|
112
|
+
Consider creating a summary note of recent activity in {project}:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
await write_note(
|
|
116
|
+
"{project}",
|
|
117
|
+
title="Activity Summary {timeframe}",
|
|
118
|
+
content='''
|
|
119
|
+
# Activity Summary for {project} ({timeframe})
|
|
120
|
+
|
|
121
|
+
## Overview
|
|
122
|
+
[Summary of key changes and developments in this project over this period]
|
|
123
|
+
|
|
124
|
+
## Key Updates
|
|
125
|
+
[List main updates and their significance within this project]
|
|
126
|
+
|
|
127
|
+
## Observations
|
|
128
|
+
- [trend] [Observation about patterns in recent activity]
|
|
129
|
+
- [insight] [Connection between different activities]
|
|
130
|
+
|
|
131
|
+
## Relations
|
|
132
|
+
- summarizes [[{first_title}]]
|
|
133
|
+
- relates_to [[{project} Overview]]
|
|
134
|
+
''',
|
|
135
|
+
folder="summaries"
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Summarizing periodic activity helps create high-level insights and connections within the project.
|
|
140
|
+
"""
|
|
141
|
+
else:
|
|
142
|
+
# Discovery mode suggestions
|
|
143
|
+
project_count = len(recent.projects) if isinstance(recent, ProjectActivitySummary) else 0
|
|
144
|
+
most_active = (
|
|
145
|
+
getattr(recent.summary, "most_active_project", "Unknown")
|
|
146
|
+
if isinstance(recent, ProjectActivitySummary)
|
|
147
|
+
else "Unknown"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
capture_suggestions = f"""
|
|
151
|
+
## Cross-Project Activity Discovery
|
|
152
|
+
|
|
153
|
+
Found activity across {project_count} projects. Most active: **{most_active}**
|
|
154
|
+
|
|
155
|
+
Consider creating a cross-project summary:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
await write_note(
|
|
159
|
+
"{most_active if most_active != "Unknown" else "main"}",
|
|
160
|
+
title="Cross-Project Activity Summary {timeframe}",
|
|
161
|
+
content='''
|
|
162
|
+
# Cross-Project Activity Summary ({timeframe})
|
|
163
|
+
|
|
164
|
+
## Overview
|
|
165
|
+
Activity found across {project_count} projects, with {most_active} showing the most activity.
|
|
166
|
+
|
|
167
|
+
## Key Developments
|
|
168
|
+
[Summarize important changes across all projects]
|
|
169
|
+
|
|
170
|
+
## Project Insights
|
|
171
|
+
[Note patterns or connections between projects]
|
|
172
|
+
|
|
173
|
+
## Observations
|
|
174
|
+
- [trend] [Cross-project patterns observed]
|
|
175
|
+
- [insight] [Connections between different project activities]
|
|
176
|
+
|
|
177
|
+
## Relations
|
|
178
|
+
- summarizes [[{first_title}]]
|
|
179
|
+
- relates_to [[Project Portfolio Overview]]
|
|
180
|
+
''',
|
|
181
|
+
folder="summaries"
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Cross-project summaries help identify broader trends and project interconnections.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
return prompt_context + capture_suggestions
|