basic-memory 0.17.1__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.
- basic_memory/__init__.py +7 -0
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +185 -0
- basic_memory/alembic/migrations.py +24 -0
- basic_memory/alembic/script.py.mako +26 -0
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -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/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/api/__init__.py +5 -0
- basic_memory/api/app.py +131 -0
- basic_memory/api/routers/__init__.py +11 -0
- 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 +318 -0
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +90 -0
- basic_memory/api/routers/project_router.py +448 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +249 -0
- basic_memory/api/routers/search_router.py +36 -0
- 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 +182 -0
- basic_memory/api/v2/routers/knowledge_router.py +413 -0
- basic_memory/api/v2/routers/memory_router.py +130 -0
- basic_memory/api/v2/routers/project_router.py +342 -0
- basic_memory/api/v2/routers/prompt_router.py +270 -0
- basic_memory/api/v2/routers/resource_router.py +286 -0
- basic_memory/api/v2/routers/search_router.py +73 -0
- basic_memory/cli/__init__.py +1 -0
- basic_memory/cli/app.py +84 -0
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/__init__.py +18 -0
- basic_memory/cli/commands/cloud/__init__.py +6 -0
- basic_memory/cli/commands/cloud/api_client.py +112 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
- basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
- basic_memory/cli/commands/cloud/core_commands.py +195 -0
- basic_memory/cli/commands/cloud/rclone_commands.py +371 -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 +233 -0
- basic_memory/cli/commands/cloud/upload_command.py +124 -0
- basic_memory/cli/commands/command_utils.py +77 -0
- basic_memory/cli/commands/db.py +44 -0
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +84 -0
- basic_memory/cli/commands/import_claude_conversations.py +87 -0
- basic_memory/cli/commands/import_claude_projects.py +86 -0
- basic_memory/cli/commands/import_memory_json.py +87 -0
- basic_memory/cli/commands/mcp.py +76 -0
- basic_memory/cli/commands/project.py +889 -0
- basic_memory/cli/commands/status.py +174 -0
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/main.py +28 -0
- basic_memory/config.py +616 -0
- basic_memory/db.py +394 -0
- basic_memory/deps.py +705 -0
- basic_memory/file_utils.py +478 -0
- basic_memory/ignore_utils.py +297 -0
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +232 -0
- basic_memory/importers/claude_conversations_importer.py +180 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +108 -0
- basic_memory/importers/utils.py +61 -0
- basic_memory/markdown/__init__.py +21 -0
- basic_memory/markdown/entity_parser.py +279 -0
- basic_memory/markdown/markdown_processor.py +160 -0
- basic_memory/markdown/plugins.py +242 -0
- basic_memory/markdown/schemas.py +70 -0
- basic_memory/markdown/utils.py +117 -0
- basic_memory/mcp/__init__.py +1 -0
- basic_memory/mcp/async_client.py +139 -0
- basic_memory/mcp/project_context.py +141 -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 +81 -0
- basic_memory/mcp/tools/__init__.py +48 -0
- 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 +242 -0
- basic_memory/mcp/tools/edit_note.py +324 -0
- basic_memory/mcp/tools/list_directory.py +168 -0
- basic_memory/mcp/tools/move_note.py +551 -0
- basic_memory/mcp/tools/project_management.py +201 -0
- basic_memory/mcp/tools/read_content.py +281 -0
- basic_memory/mcp/tools/read_note.py +267 -0
- basic_memory/mcp/tools/recent_activity.py +534 -0
- basic_memory/mcp/tools/search.py +385 -0
- basic_memory/mcp/tools/utils.py +540 -0
- basic_memory/mcp/tools/view_note.py +78 -0
- basic_memory/mcp/tools/write_note.py +230 -0
- basic_memory/models/__init__.py +15 -0
- basic_memory/models/base.py +10 -0
- basic_memory/models/knowledge.py +226 -0
- basic_memory/models/project.py +87 -0
- basic_memory/models/search.py +85 -0
- basic_memory/repository/__init__.py +11 -0
- basic_memory/repository/entity_repository.py +503 -0
- basic_memory/repository/observation_repository.py +73 -0
- basic_memory/repository/postgres_search_repository.py +379 -0
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +128 -0
- basic_memory/repository/relation_repository.py +146 -0
- basic_memory/repository/repository.py +385 -0
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +94 -0
- basic_memory/repository/search_repository_base.py +241 -0
- basic_memory/repository/sqlite_search_repository.py +439 -0
- basic_memory/schemas/__init__.py +86 -0
- basic_memory/schemas/base.py +297 -0
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/delete.py +37 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +285 -0
- basic_memory/schemas/project_info.py +212 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +112 -0
- basic_memory/schemas/response.py +229 -0
- basic_memory/schemas/search.py +117 -0
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/schemas/v2/__init__.py +27 -0
- basic_memory/schemas/v2/entity.py +129 -0
- basic_memory/schemas/v2/resource.py +46 -0
- basic_memory/services/__init__.py +8 -0
- basic_memory/services/context_service.py +601 -0
- basic_memory/services/directory_service.py +308 -0
- basic_memory/services/entity_service.py +864 -0
- basic_memory/services/exceptions.py +37 -0
- basic_memory/services/file_service.py +541 -0
- basic_memory/services/initialization.py +216 -0
- basic_memory/services/link_resolver.py +121 -0
- basic_memory/services/project_service.py +880 -0
- basic_memory/services/search_service.py +404 -0
- basic_memory/services/service.py +15 -0
- basic_memory/sync/__init__.py +6 -0
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/sync_service.py +1259 -0
- basic_memory/sync/watch_service.py +510 -0
- 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 +468 -0
- basic_memory-0.17.1.dist-info/METADATA +617 -0
- basic_memory-0.17.1.dist-info/RECORD +171 -0
- basic_memory-0.17.1.dist-info/WHEEL +4 -0
- basic_memory-0.17.1.dist-info/entry_points.txt +3 -0
- basic_memory-0.17.1.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Schema models for entity markdown files."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Observation(BaseModel):
|
|
10
|
+
"""An observation about an entity."""
|
|
11
|
+
|
|
12
|
+
category: Optional[str] = "Note"
|
|
13
|
+
content: str
|
|
14
|
+
tags: Optional[List[str]] = None
|
|
15
|
+
context: Optional[str] = None
|
|
16
|
+
|
|
17
|
+
def __str__(self) -> str:
|
|
18
|
+
obs_string = f"- [{self.category}] {self.content}"
|
|
19
|
+
if self.context:
|
|
20
|
+
obs_string += f" ({self.context})"
|
|
21
|
+
return obs_string
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Relation(BaseModel):
|
|
25
|
+
"""A relation between entities."""
|
|
26
|
+
|
|
27
|
+
type: str
|
|
28
|
+
target: str
|
|
29
|
+
context: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
rel_string = f"- {self.type} [[{self.target}]]"
|
|
33
|
+
if self.context:
|
|
34
|
+
rel_string += f" ({self.context})"
|
|
35
|
+
return rel_string
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class EntityFrontmatter(BaseModel):
|
|
39
|
+
"""Required frontmatter fields for an entity."""
|
|
40
|
+
|
|
41
|
+
metadata: dict = {}
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def tags(self) -> List[str]:
|
|
45
|
+
return self.metadata.get("tags") if self.metadata else None # pyright: ignore
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def title(self) -> str:
|
|
49
|
+
return self.metadata.get("title") if self.metadata else None # pyright: ignore
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def type(self) -> str:
|
|
53
|
+
return self.metadata.get("type", "note") if self.metadata else "note" # pyright: ignore
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def permalink(self) -> str:
|
|
57
|
+
return self.metadata.get("permalink") if self.metadata else None # pyright: ignore
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class EntityMarkdown(BaseModel):
|
|
61
|
+
"""Complete entity combining frontmatter, content, and metadata."""
|
|
62
|
+
|
|
63
|
+
frontmatter: EntityFrontmatter
|
|
64
|
+
content: Optional[str] = None
|
|
65
|
+
observations: List[Observation] = []
|
|
66
|
+
relations: List[Relation] = []
|
|
67
|
+
|
|
68
|
+
# created, updated will have values after a read
|
|
69
|
+
created: Optional[datetime] = None
|
|
70
|
+
modified: Optional[datetime] = None
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Utilities for converting between markdown and entity models."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from frontmatter import Post
|
|
8
|
+
|
|
9
|
+
from basic_memory.file_utils import has_frontmatter, remove_frontmatter, parse_frontmatter
|
|
10
|
+
from basic_memory.markdown import EntityMarkdown
|
|
11
|
+
from basic_memory.models import Entity
|
|
12
|
+
from basic_memory.models import Observation as ObservationModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def entity_model_from_markdown(
|
|
16
|
+
file_path: Path,
|
|
17
|
+
markdown: EntityMarkdown,
|
|
18
|
+
entity: Optional[Entity] = None,
|
|
19
|
+
project_id: Optional[int] = None,
|
|
20
|
+
) -> Entity:
|
|
21
|
+
"""
|
|
22
|
+
Convert markdown entity to model. Does not include relations.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
file_path: Path to the markdown file
|
|
26
|
+
markdown: Parsed markdown entity
|
|
27
|
+
entity: Optional existing entity to update
|
|
28
|
+
project_id: Project ID for new observations (uses entity.project_id if not provided)
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Entity model populated from markdown
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ValueError: If required datetime fields are missing from markdown
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
if not markdown.created or not markdown.modified: # pragma: no cover
|
|
38
|
+
raise ValueError("Both created and modified dates are required in markdown")
|
|
39
|
+
|
|
40
|
+
# Create or update entity
|
|
41
|
+
model = entity or Entity()
|
|
42
|
+
|
|
43
|
+
# Update basic fields
|
|
44
|
+
model.title = markdown.frontmatter.title
|
|
45
|
+
model.entity_type = markdown.frontmatter.type
|
|
46
|
+
# Only update permalink if it exists in frontmatter, otherwise preserve existing
|
|
47
|
+
if markdown.frontmatter.permalink is not None:
|
|
48
|
+
model.permalink = markdown.frontmatter.permalink
|
|
49
|
+
model.file_path = file_path.as_posix()
|
|
50
|
+
model.content_type = "text/markdown"
|
|
51
|
+
model.created_at = markdown.created
|
|
52
|
+
model.updated_at = markdown.modified
|
|
53
|
+
|
|
54
|
+
# Handle metadata - ensure all values are strings and filter None
|
|
55
|
+
metadata = markdown.frontmatter.metadata or {}
|
|
56
|
+
model.entity_metadata = {k: str(v) for k, v in metadata.items() if v is not None}
|
|
57
|
+
|
|
58
|
+
# Get project_id from entity if not provided
|
|
59
|
+
obs_project_id = project_id or (model.project_id if hasattr(model, "project_id") else None)
|
|
60
|
+
|
|
61
|
+
# Convert observations
|
|
62
|
+
model.observations = [
|
|
63
|
+
ObservationModel(
|
|
64
|
+
project_id=obs_project_id,
|
|
65
|
+
content=obs.content,
|
|
66
|
+
category=obs.category,
|
|
67
|
+
context=obs.context,
|
|
68
|
+
tags=obs.tags,
|
|
69
|
+
)
|
|
70
|
+
for obs in markdown.observations
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
return model
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def schema_to_markdown(schema: Any) -> Post:
|
|
77
|
+
"""
|
|
78
|
+
Convert schema to markdown Post object.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
schema: Schema to convert (must have title, entity_type, and permalink attributes)
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Post object with frontmatter metadata
|
|
85
|
+
"""
|
|
86
|
+
# Extract content and metadata
|
|
87
|
+
content = schema.content or ""
|
|
88
|
+
entity_metadata = dict(schema.entity_metadata or {})
|
|
89
|
+
|
|
90
|
+
# if the content contains frontmatter, remove it and merge
|
|
91
|
+
if has_frontmatter(content):
|
|
92
|
+
content_frontmatter = parse_frontmatter(content)
|
|
93
|
+
content = remove_frontmatter(content)
|
|
94
|
+
|
|
95
|
+
# Merge content frontmatter with entity metadata
|
|
96
|
+
# (entity_metadata takes precedence for conflicts)
|
|
97
|
+
content_frontmatter.update(entity_metadata)
|
|
98
|
+
entity_metadata = content_frontmatter
|
|
99
|
+
|
|
100
|
+
# Remove special fields for ordered frontmatter
|
|
101
|
+
for field in ["type", "title", "permalink"]:
|
|
102
|
+
entity_metadata.pop(field, None)
|
|
103
|
+
|
|
104
|
+
# Create Post with fields ordered by insert order
|
|
105
|
+
post = Post(
|
|
106
|
+
content,
|
|
107
|
+
title=schema.title,
|
|
108
|
+
type=schema.entity_type,
|
|
109
|
+
)
|
|
110
|
+
# set the permalink if passed in
|
|
111
|
+
if schema.permalink:
|
|
112
|
+
post.metadata["permalink"] = schema.permalink
|
|
113
|
+
|
|
114
|
+
if entity_metadata:
|
|
115
|
+
post.metadata.update(entity_metadata)
|
|
116
|
+
|
|
117
|
+
return post
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP server for basic-memory."""
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager, AbstractAsyncContextManager
|
|
2
|
+
from typing import AsyncIterator, Callable, Optional
|
|
3
|
+
|
|
4
|
+
from httpx import ASGITransport, AsyncClient, Timeout
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from basic_memory.api.app import app as fastapi_app
|
|
8
|
+
from basic_memory.config import ConfigManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Optional factory override for dependency injection
|
|
12
|
+
_client_factory: Optional[Callable[[], AbstractAsyncContextManager[AsyncClient]]] = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def set_client_factory(factory: Callable[[], AbstractAsyncContextManager[AsyncClient]]) -> None:
|
|
16
|
+
"""Override the default client factory (for cloud app, testing, etc).
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
factory: An async context manager that yields an AsyncClient
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
@asynccontextmanager
|
|
23
|
+
async def custom_client_factory():
|
|
24
|
+
async with AsyncClient(...) as client:
|
|
25
|
+
yield client
|
|
26
|
+
|
|
27
|
+
set_client_factory(custom_client_factory)
|
|
28
|
+
"""
|
|
29
|
+
global _client_factory
|
|
30
|
+
_client_factory = factory
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@asynccontextmanager
|
|
34
|
+
async def get_client() -> AsyncIterator[AsyncClient]:
|
|
35
|
+
"""Get an AsyncClient as a context manager.
|
|
36
|
+
|
|
37
|
+
This function provides proper resource management for HTTP clients,
|
|
38
|
+
ensuring connections are closed after use. It supports three modes:
|
|
39
|
+
|
|
40
|
+
1. **Factory injection** (cloud app, tests):
|
|
41
|
+
If a custom factory is set via set_client_factory(), use that.
|
|
42
|
+
|
|
43
|
+
2. **CLI cloud mode**:
|
|
44
|
+
When cloud_mode_enabled is True, create HTTP client with auth
|
|
45
|
+
token from CLIAuth for requests to cloud proxy endpoint.
|
|
46
|
+
|
|
47
|
+
3. **Local mode** (default):
|
|
48
|
+
Use ASGI transport for in-process requests to local FastAPI app.
|
|
49
|
+
|
|
50
|
+
Usage:
|
|
51
|
+
async with get_client() as client:
|
|
52
|
+
response = await client.get("/path")
|
|
53
|
+
|
|
54
|
+
Yields:
|
|
55
|
+
AsyncClient: Configured HTTP client for the current mode
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
RuntimeError: If cloud mode is enabled but user is not authenticated
|
|
59
|
+
"""
|
|
60
|
+
if _client_factory:
|
|
61
|
+
# Use injected factory (cloud app, tests)
|
|
62
|
+
async with _client_factory() as client:
|
|
63
|
+
yield client
|
|
64
|
+
else:
|
|
65
|
+
# Default: create based on config
|
|
66
|
+
config = ConfigManager().config
|
|
67
|
+
timeout = Timeout(
|
|
68
|
+
connect=10.0, # 10 seconds for connection
|
|
69
|
+
read=30.0, # 30 seconds for reading response
|
|
70
|
+
write=30.0, # 30 seconds for writing request
|
|
71
|
+
pool=30.0, # 30 seconds for connection pool
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if config.cloud_mode_enabled:
|
|
75
|
+
# CLI cloud mode: inject auth when creating client
|
|
76
|
+
from basic_memory.cli.auth import CLIAuth
|
|
77
|
+
|
|
78
|
+
auth = CLIAuth(client_id=config.cloud_client_id, authkit_domain=config.cloud_domain)
|
|
79
|
+
token = await auth.get_valid_token()
|
|
80
|
+
|
|
81
|
+
if not token:
|
|
82
|
+
raise RuntimeError(
|
|
83
|
+
"Cloud mode enabled but not authenticated. "
|
|
84
|
+
"Run 'basic-memory cloud login' first."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Auth header set ONCE at client creation
|
|
88
|
+
proxy_base_url = f"{config.cloud_host}/proxy"
|
|
89
|
+
logger.info(f"Creating HTTP client for cloud proxy at: {proxy_base_url}")
|
|
90
|
+
async with AsyncClient(
|
|
91
|
+
base_url=proxy_base_url,
|
|
92
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
93
|
+
timeout=timeout,
|
|
94
|
+
) as client:
|
|
95
|
+
yield client
|
|
96
|
+
else:
|
|
97
|
+
# Local mode: ASGI transport for in-process calls
|
|
98
|
+
# Note: ASGI transport does NOT trigger FastAPI lifespan, so no special handling needed
|
|
99
|
+
logger.info("Creating ASGI client for local Basic Memory API")
|
|
100
|
+
async with AsyncClient(
|
|
101
|
+
transport=ASGITransport(app=fastapi_app), base_url="http://test", timeout=timeout
|
|
102
|
+
) as client:
|
|
103
|
+
yield client
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def create_client() -> AsyncClient:
|
|
107
|
+
"""Create an HTTP client based on configuration.
|
|
108
|
+
|
|
109
|
+
DEPRECATED: Use get_client() context manager instead for proper resource management.
|
|
110
|
+
|
|
111
|
+
This function is kept for backward compatibility but will be removed in a future version.
|
|
112
|
+
The returned client should be closed manually by calling await client.aclose().
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
AsyncClient configured for either local ASGI or remote proxy
|
|
116
|
+
"""
|
|
117
|
+
config_manager = ConfigManager()
|
|
118
|
+
config = config_manager.config
|
|
119
|
+
|
|
120
|
+
# Configure timeout for longer operations like write_note
|
|
121
|
+
# Default httpx timeout is 5 seconds which is too short for file operations
|
|
122
|
+
timeout = Timeout(
|
|
123
|
+
connect=10.0, # 10 seconds for connection
|
|
124
|
+
read=30.0, # 30 seconds for reading response
|
|
125
|
+
write=30.0, # 30 seconds for writing request
|
|
126
|
+
pool=30.0, # 30 seconds for connection pool
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if config.cloud_mode_enabled:
|
|
130
|
+
# Use HTTP transport to proxy endpoint
|
|
131
|
+
proxy_base_url = f"{config.cloud_host}/proxy"
|
|
132
|
+
logger.info(f"Creating HTTP client for proxy at: {proxy_base_url}")
|
|
133
|
+
return AsyncClient(base_url=proxy_base_url, timeout=timeout)
|
|
134
|
+
else:
|
|
135
|
+
# Default: use ASGI transport for local API (development mode)
|
|
136
|
+
logger.info("Creating ASGI client for local Basic Memory API")
|
|
137
|
+
return AsyncClient(
|
|
138
|
+
transport=ASGITransport(app=fastapi_app), base_url="http://test", timeout=timeout
|
|
139
|
+
)
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional, List
|
|
9
|
+
from httpx import AsyncClient
|
|
10
|
+
from httpx._types import (
|
|
11
|
+
HeaderTypes,
|
|
12
|
+
)
|
|
13
|
+
from loguru import logger
|
|
14
|
+
from fastmcp import Context
|
|
15
|
+
|
|
16
|
+
from basic_memory.config import ConfigManager
|
|
17
|
+
from basic_memory.mcp.tools.utils import call_get
|
|
18
|
+
from basic_memory.schemas.project_info import ProjectItem, ProjectList
|
|
19
|
+
from basic_memory.utils import generate_permalink
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def resolve_project_parameter(project: Optional[str] = None) -> Optional[str]:
|
|
23
|
+
"""Resolve project parameter using three-tier hierarchy.
|
|
24
|
+
|
|
25
|
+
if config.cloud_mode:
|
|
26
|
+
project is required
|
|
27
|
+
else:
|
|
28
|
+
Resolution order:
|
|
29
|
+
1. Single Project Mode (--project cli arg, or BASIC_MEMORY_MCP_PROJECT env var) - highest priority
|
|
30
|
+
2. Explicit project parameter - medium priority
|
|
31
|
+
3. Default project if default_project_mode=true - lowest priority
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
project: Optional explicit project parameter
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Resolved project name or None if no resolution possible
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
config = ConfigManager().config
|
|
41
|
+
# if cloud_mode, project is required
|
|
42
|
+
if config.cloud_mode:
|
|
43
|
+
if project:
|
|
44
|
+
logger.debug(f"project: {project}, cloud_mode: {config.cloud_mode}")
|
|
45
|
+
return project
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError("No project specified. Project is required for cloud mode.")
|
|
48
|
+
|
|
49
|
+
# Priority 1: CLI constraint overrides everything (--project arg sets env var)
|
|
50
|
+
constrained_project = os.environ.get("BASIC_MEMORY_MCP_PROJECT")
|
|
51
|
+
if constrained_project:
|
|
52
|
+
logger.debug(f"Using CLI constrained project: {constrained_project}")
|
|
53
|
+
return constrained_project
|
|
54
|
+
|
|
55
|
+
# Priority 2: Explicit project parameter
|
|
56
|
+
if project:
|
|
57
|
+
logger.debug(f"Using explicit project parameter: {project}")
|
|
58
|
+
return project
|
|
59
|
+
|
|
60
|
+
# Priority 3: Default project mode
|
|
61
|
+
if config.default_project_mode:
|
|
62
|
+
logger.debug(f"Using default project from config: {config.default_project}")
|
|
63
|
+
return config.default_project
|
|
64
|
+
|
|
65
|
+
# No resolution possible
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def get_project_names(client: AsyncClient, headers: HeaderTypes | None = None) -> List[str]:
|
|
70
|
+
response = await call_get(client, "/projects/projects", headers=headers)
|
|
71
|
+
project_list = ProjectList.model_validate(response.json())
|
|
72
|
+
return [project.name for project in project_list.projects]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def get_active_project(
|
|
76
|
+
client: AsyncClient,
|
|
77
|
+
project: Optional[str] = None,
|
|
78
|
+
context: Optional[Context] = None,
|
|
79
|
+
headers: HeaderTypes | None = None,
|
|
80
|
+
) -> ProjectItem:
|
|
81
|
+
"""Get and validate project, setting it in context if available.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
client: HTTP client for API calls
|
|
85
|
+
project: Optional project name (resolved using hierarchy)
|
|
86
|
+
context: Optional FastMCP context to cache the result
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The validated project item
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValueError: If no project can be resolved
|
|
93
|
+
HTTPError: If project doesn't exist or is inaccessible
|
|
94
|
+
"""
|
|
95
|
+
resolved_project = await resolve_project_parameter(project)
|
|
96
|
+
if not resolved_project:
|
|
97
|
+
project_names = await get_project_names(client, headers)
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"No project specified. "
|
|
100
|
+
"Either set 'default_project_mode=true' in config, or use 'project' argument.\n"
|
|
101
|
+
f"Available projects: {project_names}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
project = resolved_project
|
|
105
|
+
|
|
106
|
+
# Check if already cached in context
|
|
107
|
+
if context:
|
|
108
|
+
cached_project = context.get_state("active_project")
|
|
109
|
+
if cached_project and cached_project.name == project:
|
|
110
|
+
logger.debug(f"Using cached project from context: {project}")
|
|
111
|
+
return cached_project
|
|
112
|
+
|
|
113
|
+
# Validate project exists by calling API
|
|
114
|
+
logger.debug(f"Validating project: {project}")
|
|
115
|
+
permalink = generate_permalink(project)
|
|
116
|
+
response = await call_get(client, f"/{permalink}/project/item", headers=headers)
|
|
117
|
+
active_project = ProjectItem.model_validate(response.json())
|
|
118
|
+
|
|
119
|
+
# Cache in context if available
|
|
120
|
+
if context:
|
|
121
|
+
context.set_state("active_project", active_project)
|
|
122
|
+
logger.debug(f"Cached project in context: {project}")
|
|
123
|
+
|
|
124
|
+
logger.debug(f"Validated project: {active_project.name}")
|
|
125
|
+
return active_project
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def add_project_metadata(result: str, project_name: str) -> str:
|
|
129
|
+
"""Add project context as metadata footer for assistant session tracking.
|
|
130
|
+
|
|
131
|
+
Provides clear project context to help the assistant remember which
|
|
132
|
+
project is being used throughout the conversation session.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
result: The tool result string
|
|
136
|
+
project_name: The project name that was used
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Result with project session tracking metadata
|
|
140
|
+
"""
|
|
141
|
+
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:
|
|
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:
|
|
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"]
|