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.
Files changed (171) hide show
  1. basic_memory/__init__.py +7 -0
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +185 -0
  4. basic_memory/alembic/migrations.py +24 -0
  5. basic_memory/alembic/script.py.mako +26 -0
  6. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  7. basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -0
  8. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  9. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
  10. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
  11. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  12. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  13. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  14. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  15. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
  16. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  17. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  18. basic_memory/api/__init__.py +5 -0
  19. basic_memory/api/app.py +131 -0
  20. basic_memory/api/routers/__init__.py +11 -0
  21. basic_memory/api/routers/directory_router.py +84 -0
  22. basic_memory/api/routers/importer_router.py +152 -0
  23. basic_memory/api/routers/knowledge_router.py +318 -0
  24. basic_memory/api/routers/management_router.py +80 -0
  25. basic_memory/api/routers/memory_router.py +90 -0
  26. basic_memory/api/routers/project_router.py +448 -0
  27. basic_memory/api/routers/prompt_router.py +260 -0
  28. basic_memory/api/routers/resource_router.py +249 -0
  29. basic_memory/api/routers/search_router.py +36 -0
  30. basic_memory/api/routers/utils.py +169 -0
  31. basic_memory/api/template_loader.py +292 -0
  32. basic_memory/api/v2/__init__.py +35 -0
  33. basic_memory/api/v2/routers/__init__.py +21 -0
  34. basic_memory/api/v2/routers/directory_router.py +93 -0
  35. basic_memory/api/v2/routers/importer_router.py +182 -0
  36. basic_memory/api/v2/routers/knowledge_router.py +413 -0
  37. basic_memory/api/v2/routers/memory_router.py +130 -0
  38. basic_memory/api/v2/routers/project_router.py +342 -0
  39. basic_memory/api/v2/routers/prompt_router.py +270 -0
  40. basic_memory/api/v2/routers/resource_router.py +286 -0
  41. basic_memory/api/v2/routers/search_router.py +73 -0
  42. basic_memory/cli/__init__.py +1 -0
  43. basic_memory/cli/app.py +84 -0
  44. basic_memory/cli/auth.py +277 -0
  45. basic_memory/cli/commands/__init__.py +18 -0
  46. basic_memory/cli/commands/cloud/__init__.py +6 -0
  47. basic_memory/cli/commands/cloud/api_client.py +112 -0
  48. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  49. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  50. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  51. basic_memory/cli/commands/cloud/rclone_commands.py +371 -0
  52. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  53. basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
  54. basic_memory/cli/commands/cloud/upload.py +233 -0
  55. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  56. basic_memory/cli/commands/command_utils.py +77 -0
  57. basic_memory/cli/commands/db.py +44 -0
  58. basic_memory/cli/commands/format.py +198 -0
  59. basic_memory/cli/commands/import_chatgpt.py +84 -0
  60. basic_memory/cli/commands/import_claude_conversations.py +87 -0
  61. basic_memory/cli/commands/import_claude_projects.py +86 -0
  62. basic_memory/cli/commands/import_memory_json.py +87 -0
  63. basic_memory/cli/commands/mcp.py +76 -0
  64. basic_memory/cli/commands/project.py +889 -0
  65. basic_memory/cli/commands/status.py +174 -0
  66. basic_memory/cli/commands/telemetry.py +81 -0
  67. basic_memory/cli/commands/tool.py +341 -0
  68. basic_memory/cli/main.py +28 -0
  69. basic_memory/config.py +616 -0
  70. basic_memory/db.py +394 -0
  71. basic_memory/deps.py +705 -0
  72. basic_memory/file_utils.py +478 -0
  73. basic_memory/ignore_utils.py +297 -0
  74. basic_memory/importers/__init__.py +27 -0
  75. basic_memory/importers/base.py +79 -0
  76. basic_memory/importers/chatgpt_importer.py +232 -0
  77. basic_memory/importers/claude_conversations_importer.py +180 -0
  78. basic_memory/importers/claude_projects_importer.py +148 -0
  79. basic_memory/importers/memory_json_importer.py +108 -0
  80. basic_memory/importers/utils.py +61 -0
  81. basic_memory/markdown/__init__.py +21 -0
  82. basic_memory/markdown/entity_parser.py +279 -0
  83. basic_memory/markdown/markdown_processor.py +160 -0
  84. basic_memory/markdown/plugins.py +242 -0
  85. basic_memory/markdown/schemas.py +70 -0
  86. basic_memory/markdown/utils.py +117 -0
  87. basic_memory/mcp/__init__.py +1 -0
  88. basic_memory/mcp/async_client.py +139 -0
  89. basic_memory/mcp/project_context.py +141 -0
  90. basic_memory/mcp/prompts/__init__.py +19 -0
  91. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  92. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  93. basic_memory/mcp/prompts/recent_activity.py +188 -0
  94. basic_memory/mcp/prompts/search.py +57 -0
  95. basic_memory/mcp/prompts/utils.py +162 -0
  96. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  97. basic_memory/mcp/resources/project_info.py +71 -0
  98. basic_memory/mcp/server.py +81 -0
  99. basic_memory/mcp/tools/__init__.py +48 -0
  100. basic_memory/mcp/tools/build_context.py +120 -0
  101. basic_memory/mcp/tools/canvas.py +152 -0
  102. basic_memory/mcp/tools/chatgpt_tools.py +190 -0
  103. basic_memory/mcp/tools/delete_note.py +242 -0
  104. basic_memory/mcp/tools/edit_note.py +324 -0
  105. basic_memory/mcp/tools/list_directory.py +168 -0
  106. basic_memory/mcp/tools/move_note.py +551 -0
  107. basic_memory/mcp/tools/project_management.py +201 -0
  108. basic_memory/mcp/tools/read_content.py +281 -0
  109. basic_memory/mcp/tools/read_note.py +267 -0
  110. basic_memory/mcp/tools/recent_activity.py +534 -0
  111. basic_memory/mcp/tools/search.py +385 -0
  112. basic_memory/mcp/tools/utils.py +540 -0
  113. basic_memory/mcp/tools/view_note.py +78 -0
  114. basic_memory/mcp/tools/write_note.py +230 -0
  115. basic_memory/models/__init__.py +15 -0
  116. basic_memory/models/base.py +10 -0
  117. basic_memory/models/knowledge.py +226 -0
  118. basic_memory/models/project.py +87 -0
  119. basic_memory/models/search.py +85 -0
  120. basic_memory/repository/__init__.py +11 -0
  121. basic_memory/repository/entity_repository.py +503 -0
  122. basic_memory/repository/observation_repository.py +73 -0
  123. basic_memory/repository/postgres_search_repository.py +379 -0
  124. basic_memory/repository/project_info_repository.py +10 -0
  125. basic_memory/repository/project_repository.py +128 -0
  126. basic_memory/repository/relation_repository.py +146 -0
  127. basic_memory/repository/repository.py +385 -0
  128. basic_memory/repository/search_index_row.py +95 -0
  129. basic_memory/repository/search_repository.py +94 -0
  130. basic_memory/repository/search_repository_base.py +241 -0
  131. basic_memory/repository/sqlite_search_repository.py +439 -0
  132. basic_memory/schemas/__init__.py +86 -0
  133. basic_memory/schemas/base.py +297 -0
  134. basic_memory/schemas/cloud.py +50 -0
  135. basic_memory/schemas/delete.py +37 -0
  136. basic_memory/schemas/directory.py +30 -0
  137. basic_memory/schemas/importer.py +35 -0
  138. basic_memory/schemas/memory.py +285 -0
  139. basic_memory/schemas/project_info.py +212 -0
  140. basic_memory/schemas/prompt.py +90 -0
  141. basic_memory/schemas/request.py +112 -0
  142. basic_memory/schemas/response.py +229 -0
  143. basic_memory/schemas/search.py +117 -0
  144. basic_memory/schemas/sync_report.py +72 -0
  145. basic_memory/schemas/v2/__init__.py +27 -0
  146. basic_memory/schemas/v2/entity.py +129 -0
  147. basic_memory/schemas/v2/resource.py +46 -0
  148. basic_memory/services/__init__.py +8 -0
  149. basic_memory/services/context_service.py +601 -0
  150. basic_memory/services/directory_service.py +308 -0
  151. basic_memory/services/entity_service.py +864 -0
  152. basic_memory/services/exceptions.py +37 -0
  153. basic_memory/services/file_service.py +541 -0
  154. basic_memory/services/initialization.py +216 -0
  155. basic_memory/services/link_resolver.py +121 -0
  156. basic_memory/services/project_service.py +880 -0
  157. basic_memory/services/search_service.py +404 -0
  158. basic_memory/services/service.py +15 -0
  159. basic_memory/sync/__init__.py +6 -0
  160. basic_memory/sync/background_sync.py +26 -0
  161. basic_memory/sync/sync_service.py +1259 -0
  162. basic_memory/sync/watch_service.py +510 -0
  163. basic_memory/telemetry.py +249 -0
  164. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  165. basic_memory/templates/prompts/search.hbs +101 -0
  166. basic_memory/utils.py +468 -0
  167. basic_memory-0.17.1.dist-info/METADATA +617 -0
  168. basic_memory-0.17.1.dist-info/RECORD +171 -0
  169. basic_memory-0.17.1.dist-info/WHEEL +4 -0
  170. basic_memory-0.17.1.dist-info/entry_points.txt +3 -0
  171. 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"]