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.

Files changed (195) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +130 -20
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  6. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  7. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
  8. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
  9. basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
  10. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  11. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  12. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  13. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  14. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
  15. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  16. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  17. basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
  18. basic_memory/api/app.py +87 -20
  19. basic_memory/api/container.py +133 -0
  20. basic_memory/api/routers/__init__.py +4 -1
  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 +180 -23
  24. basic_memory/api/routers/management_router.py +80 -0
  25. basic_memory/api/routers/memory_router.py +9 -64
  26. basic_memory/api/routers/project_router.py +460 -0
  27. basic_memory/api/routers/prompt_router.py +260 -0
  28. basic_memory/api/routers/resource_router.py +136 -11
  29. basic_memory/api/routers/search_router.py +5 -5
  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 +181 -0
  36. basic_memory/api/v2/routers/knowledge_router.py +427 -0
  37. basic_memory/api/v2/routers/memory_router.py +130 -0
  38. basic_memory/api/v2/routers/project_router.py +359 -0
  39. basic_memory/api/v2/routers/prompt_router.py +269 -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/app.py +80 -10
  43. basic_memory/cli/auth.py +300 -0
  44. basic_memory/cli/commands/__init__.py +15 -2
  45. basic_memory/cli/commands/cloud/__init__.py +6 -0
  46. basic_memory/cli/commands/cloud/api_client.py +127 -0
  47. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  48. basic_memory/cli/commands/cloud/cloud_utils.py +108 -0
  49. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  50. basic_memory/cli/commands/cloud/rclone_commands.py +397 -0
  51. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  52. basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
  53. basic_memory/cli/commands/cloud/upload.py +240 -0
  54. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  55. basic_memory/cli/commands/command_utils.py +99 -0
  56. basic_memory/cli/commands/db.py +87 -12
  57. basic_memory/cli/commands/format.py +198 -0
  58. basic_memory/cli/commands/import_chatgpt.py +47 -223
  59. basic_memory/cli/commands/import_claude_conversations.py +48 -171
  60. basic_memory/cli/commands/import_claude_projects.py +53 -160
  61. basic_memory/cli/commands/import_memory_json.py +55 -111
  62. basic_memory/cli/commands/mcp.py +67 -11
  63. basic_memory/cli/commands/project.py +889 -0
  64. basic_memory/cli/commands/status.py +52 -34
  65. basic_memory/cli/commands/telemetry.py +81 -0
  66. basic_memory/cli/commands/tool.py +341 -0
  67. basic_memory/cli/container.py +84 -0
  68. basic_memory/cli/main.py +14 -6
  69. basic_memory/config.py +580 -26
  70. basic_memory/db.py +285 -28
  71. basic_memory/deps/__init__.py +293 -0
  72. basic_memory/deps/config.py +26 -0
  73. basic_memory/deps/db.py +56 -0
  74. basic_memory/deps/importers.py +200 -0
  75. basic_memory/deps/projects.py +238 -0
  76. basic_memory/deps/repositories.py +179 -0
  77. basic_memory/deps/services.py +480 -0
  78. basic_memory/deps.py +16 -185
  79. basic_memory/file_utils.py +318 -54
  80. basic_memory/ignore_utils.py +297 -0
  81. basic_memory/importers/__init__.py +27 -0
  82. basic_memory/importers/base.py +100 -0
  83. basic_memory/importers/chatgpt_importer.py +245 -0
  84. basic_memory/importers/claude_conversations_importer.py +192 -0
  85. basic_memory/importers/claude_projects_importer.py +184 -0
  86. basic_memory/importers/memory_json_importer.py +128 -0
  87. basic_memory/importers/utils.py +61 -0
  88. basic_memory/markdown/entity_parser.py +182 -23
  89. basic_memory/markdown/markdown_processor.py +70 -7
  90. basic_memory/markdown/plugins.py +43 -23
  91. basic_memory/markdown/schemas.py +1 -1
  92. basic_memory/markdown/utils.py +38 -14
  93. basic_memory/mcp/async_client.py +135 -4
  94. basic_memory/mcp/clients/__init__.py +28 -0
  95. basic_memory/mcp/clients/directory.py +70 -0
  96. basic_memory/mcp/clients/knowledge.py +176 -0
  97. basic_memory/mcp/clients/memory.py +120 -0
  98. basic_memory/mcp/clients/project.py +89 -0
  99. basic_memory/mcp/clients/resource.py +71 -0
  100. basic_memory/mcp/clients/search.py +65 -0
  101. basic_memory/mcp/container.py +110 -0
  102. basic_memory/mcp/project_context.py +155 -0
  103. basic_memory/mcp/prompts/__init__.py +19 -0
  104. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  105. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  106. basic_memory/mcp/prompts/recent_activity.py +188 -0
  107. basic_memory/mcp/prompts/search.py +57 -0
  108. basic_memory/mcp/prompts/utils.py +162 -0
  109. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  110. basic_memory/mcp/resources/project_info.py +71 -0
  111. basic_memory/mcp/server.py +61 -9
  112. basic_memory/mcp/tools/__init__.py +33 -21
  113. basic_memory/mcp/tools/build_context.py +120 -0
  114. basic_memory/mcp/tools/canvas.py +152 -0
  115. basic_memory/mcp/tools/chatgpt_tools.py +190 -0
  116. basic_memory/mcp/tools/delete_note.py +249 -0
  117. basic_memory/mcp/tools/edit_note.py +325 -0
  118. basic_memory/mcp/tools/list_directory.py +157 -0
  119. basic_memory/mcp/tools/move_note.py +549 -0
  120. basic_memory/mcp/tools/project_management.py +204 -0
  121. basic_memory/mcp/tools/read_content.py +281 -0
  122. basic_memory/mcp/tools/read_note.py +265 -0
  123. basic_memory/mcp/tools/recent_activity.py +528 -0
  124. basic_memory/mcp/tools/search.py +377 -24
  125. basic_memory/mcp/tools/utils.py +402 -16
  126. basic_memory/mcp/tools/view_note.py +78 -0
  127. basic_memory/mcp/tools/write_note.py +230 -0
  128. basic_memory/models/__init__.py +3 -2
  129. basic_memory/models/knowledge.py +82 -17
  130. basic_memory/models/project.py +93 -0
  131. basic_memory/models/search.py +68 -8
  132. basic_memory/project_resolver.py +222 -0
  133. basic_memory/repository/__init__.py +2 -0
  134. basic_memory/repository/entity_repository.py +437 -8
  135. basic_memory/repository/observation_repository.py +36 -3
  136. basic_memory/repository/postgres_search_repository.py +451 -0
  137. basic_memory/repository/project_info_repository.py +10 -0
  138. basic_memory/repository/project_repository.py +140 -0
  139. basic_memory/repository/relation_repository.py +79 -4
  140. basic_memory/repository/repository.py +148 -29
  141. basic_memory/repository/search_index_row.py +95 -0
  142. basic_memory/repository/search_repository.py +79 -268
  143. basic_memory/repository/search_repository_base.py +241 -0
  144. basic_memory/repository/sqlite_search_repository.py +437 -0
  145. basic_memory/runtime.py +61 -0
  146. basic_memory/schemas/__init__.py +22 -9
  147. basic_memory/schemas/base.py +131 -12
  148. basic_memory/schemas/cloud.py +50 -0
  149. basic_memory/schemas/directory.py +31 -0
  150. basic_memory/schemas/importer.py +35 -0
  151. basic_memory/schemas/memory.py +194 -25
  152. basic_memory/schemas/project_info.py +213 -0
  153. basic_memory/schemas/prompt.py +90 -0
  154. basic_memory/schemas/request.py +56 -2
  155. basic_memory/schemas/response.py +85 -28
  156. basic_memory/schemas/search.py +36 -35
  157. basic_memory/schemas/sync_report.py +72 -0
  158. basic_memory/schemas/v2/__init__.py +27 -0
  159. basic_memory/schemas/v2/entity.py +133 -0
  160. basic_memory/schemas/v2/resource.py +47 -0
  161. basic_memory/services/__init__.py +2 -1
  162. basic_memory/services/context_service.py +451 -138
  163. basic_memory/services/directory_service.py +310 -0
  164. basic_memory/services/entity_service.py +636 -71
  165. basic_memory/services/exceptions.py +21 -0
  166. basic_memory/services/file_service.py +402 -33
  167. basic_memory/services/initialization.py +216 -0
  168. basic_memory/services/link_resolver.py +50 -56
  169. basic_memory/services/project_service.py +888 -0
  170. basic_memory/services/search_service.py +232 -37
  171. basic_memory/sync/__init__.py +4 -2
  172. basic_memory/sync/background_sync.py +26 -0
  173. basic_memory/sync/coordinator.py +160 -0
  174. basic_memory/sync/sync_service.py +1200 -109
  175. basic_memory/sync/watch_service.py +432 -135
  176. basic_memory/telemetry.py +249 -0
  177. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  178. basic_memory/templates/prompts/search.hbs +101 -0
  179. basic_memory/utils.py +407 -54
  180. basic_memory-0.17.4.dist-info/METADATA +617 -0
  181. basic_memory-0.17.4.dist-info/RECORD +193 -0
  182. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
  183. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +1 -0
  184. basic_memory/alembic/README +0 -1
  185. basic_memory/cli/commands/sync.py +0 -206
  186. basic_memory/cli/commands/tools.py +0 -157
  187. basic_memory/mcp/tools/knowledge.py +0 -68
  188. basic_memory/mcp/tools/memory.py +0 -170
  189. basic_memory/mcp/tools/notes.py +0 -202
  190. basic_memory/schemas/discovery.py +0 -28
  191. basic_memory/sync/file_change_scanner.py +0 -158
  192. basic_memory/sync/utils.py +0 -31
  193. basic_memory-0.7.0.dist-info/METADATA +0 -378
  194. basic_memory-0.7.0.dist-info/RECORD +0 -82
  195. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,139 @@
1
- from httpx import ASGITransport, AsyncClient
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
2
6
 
3
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
4
119
 
5
- BASE_URL = "memory://"
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
+ )
6
128
 
7
- # Create shared async client
8
- client = AsyncClient(transport=ASGITransport(app=fastapi_app), base_url=BASE_URL)
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,28 @@
1
+ """Typed internal API clients for MCP tools.
2
+
3
+ These clients encapsulate API paths, error handling, and response validation.
4
+ MCP tools become thin adapters that call these clients and format results.
5
+
6
+ Usage:
7
+ from basic_memory.mcp.clients import KnowledgeClient, SearchClient
8
+
9
+ async with get_client() as http_client:
10
+ knowledge = KnowledgeClient(http_client, project_id)
11
+ entity = await knowledge.create_entity(entity_data)
12
+ """
13
+
14
+ from basic_memory.mcp.clients.knowledge import KnowledgeClient
15
+ from basic_memory.mcp.clients.search import SearchClient
16
+ from basic_memory.mcp.clients.memory import MemoryClient
17
+ from basic_memory.mcp.clients.directory import DirectoryClient
18
+ from basic_memory.mcp.clients.resource import ResourceClient
19
+ from basic_memory.mcp.clients.project import ProjectClient
20
+
21
+ __all__ = [
22
+ "KnowledgeClient",
23
+ "SearchClient",
24
+ "MemoryClient",
25
+ "DirectoryClient",
26
+ "ResourceClient",
27
+ "ProjectClient",
28
+ ]
@@ -0,0 +1,70 @@
1
+ """Typed client for directory API operations.
2
+
3
+ Encapsulates all /v2/projects/{project_id}/directory/* endpoints.
4
+ """
5
+
6
+ from typing import Optional, Any
7
+
8
+ from httpx import AsyncClient
9
+
10
+ from basic_memory.mcp.tools.utils import call_get
11
+
12
+
13
+ class DirectoryClient:
14
+ """Typed client for directory listing operations.
15
+
16
+ Centralizes:
17
+ - API path construction for /v2/projects/{project_id}/directory/*
18
+ - Response validation
19
+ - Consistent error handling through call_* utilities
20
+
21
+ Usage:
22
+ async with get_client() as http_client:
23
+ client = DirectoryClient(http_client, project_id)
24
+ nodes = await client.list("/", depth=2)
25
+ """
26
+
27
+ def __init__(self, http_client: AsyncClient, project_id: str):
28
+ """Initialize the directory client.
29
+
30
+ Args:
31
+ http_client: HTTPX AsyncClient for making requests
32
+ project_id: Project external_id (UUID) for API calls
33
+ """
34
+ self.http_client = http_client
35
+ self.project_id = project_id
36
+ self._base_path = f"/v2/projects/{project_id}/directory"
37
+
38
+ async def list(
39
+ self,
40
+ dir_name: str = "/",
41
+ *,
42
+ depth: int = 1,
43
+ file_name_glob: Optional[str] = None,
44
+ ) -> list[dict[str, Any]]:
45
+ """List directory contents.
46
+
47
+ Args:
48
+ dir_name: Directory path to list (default: root)
49
+ depth: How deep to traverse (default: 1)
50
+ file_name_glob: Optional glob pattern to filter files
51
+
52
+ Returns:
53
+ List of directory nodes with their contents
54
+
55
+ Raises:
56
+ ToolError: If the request fails
57
+ """
58
+ params: dict = {
59
+ "dir_name": dir_name,
60
+ "depth": depth,
61
+ }
62
+ if file_name_glob:
63
+ params["file_name_glob"] = file_name_glob
64
+
65
+ response = await call_get(
66
+ self.http_client,
67
+ f"{self._base_path}/list",
68
+ params=params,
69
+ )
70
+ return response.json()
@@ -0,0 +1,176 @@
1
+ """Typed client for knowledge/entity API operations.
2
+
3
+ Encapsulates all /v2/projects/{project_id}/knowledge/* endpoints.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from httpx import AsyncClient
9
+
10
+ from basic_memory.mcp.tools.utils import call_get, call_post, call_put, call_patch, call_delete
11
+ from basic_memory.schemas.response import EntityResponse, DeleteEntitiesResponse
12
+
13
+
14
+ class KnowledgeClient:
15
+ """Typed client for knowledge graph entity operations.
16
+
17
+ Centralizes:
18
+ - API path construction for /v2/projects/{project_id}/knowledge/*
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 = KnowledgeClient(http_client, project_id)
25
+ entity = await client.create_entity(entity_data)
26
+ """
27
+
28
+ def __init__(self, http_client: AsyncClient, project_id: str):
29
+ """Initialize the knowledge 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}/knowledge"
38
+
39
+ # --- Entity CRUD Operations ---
40
+
41
+ async def create_entity(self, entity_data: dict[str, Any]) -> EntityResponse:
42
+ """Create a new entity.
43
+
44
+ Args:
45
+ entity_data: Entity data including title, content, folder, etc.
46
+
47
+ Returns:
48
+ EntityResponse with created entity details
49
+
50
+ Raises:
51
+ ToolError: If the request fails
52
+ """
53
+ response = await call_post(
54
+ self.http_client,
55
+ f"{self._base_path}/entities",
56
+ json=entity_data,
57
+ )
58
+ return EntityResponse.model_validate(response.json())
59
+
60
+ async def update_entity(self, entity_id: str, entity_data: dict[str, Any]) -> EntityResponse:
61
+ """Update an existing entity (full replacement).
62
+
63
+ Args:
64
+ entity_id: Entity external_id (UUID)
65
+ entity_data: Complete entity data for replacement
66
+
67
+ Returns:
68
+ EntityResponse with updated entity details
69
+
70
+ Raises:
71
+ ToolError: If the request fails
72
+ """
73
+ response = await call_put(
74
+ self.http_client,
75
+ f"{self._base_path}/entities/{entity_id}",
76
+ json=entity_data,
77
+ )
78
+ return EntityResponse.model_validate(response.json())
79
+
80
+ async def get_entity(self, entity_id: str) -> EntityResponse:
81
+ """Get an entity by ID.
82
+
83
+ Args:
84
+ entity_id: Entity external_id (UUID)
85
+
86
+ Returns:
87
+ EntityResponse with entity details
88
+
89
+ Raises:
90
+ ToolError: If the entity is not found or request fails
91
+ """
92
+ response = await call_get(
93
+ self.http_client,
94
+ f"{self._base_path}/entities/{entity_id}",
95
+ )
96
+ return EntityResponse.model_validate(response.json())
97
+
98
+ async def patch_entity(self, entity_id: str, patch_data: dict[str, Any]) -> EntityResponse:
99
+ """Partially update an entity.
100
+
101
+ Args:
102
+ entity_id: Entity external_id (UUID)
103
+ patch_data: Partial entity data to update
104
+
105
+ Returns:
106
+ EntityResponse with updated entity details
107
+
108
+ Raises:
109
+ ToolError: If the request fails
110
+ """
111
+ response = await call_patch(
112
+ self.http_client,
113
+ f"{self._base_path}/entities/{entity_id}",
114
+ json=patch_data,
115
+ )
116
+ return EntityResponse.model_validate(response.json())
117
+
118
+ async def delete_entity(self, entity_id: str) -> DeleteEntitiesResponse:
119
+ """Delete an entity.
120
+
121
+ Args:
122
+ entity_id: Entity external_id (UUID)
123
+
124
+ Returns:
125
+ DeleteEntitiesResponse confirming deletion
126
+
127
+ Raises:
128
+ ToolError: If the entity is not found or request fails
129
+ """
130
+ response = await call_delete(
131
+ self.http_client,
132
+ f"{self._base_path}/entities/{entity_id}",
133
+ )
134
+ return DeleteEntitiesResponse.model_validate(response.json())
135
+
136
+ async def move_entity(self, entity_id: str, destination_path: str) -> EntityResponse:
137
+ """Move an entity to a new location.
138
+
139
+ Args:
140
+ entity_id: Entity external_id (UUID)
141
+ destination_path: New file path for the entity
142
+
143
+ Returns:
144
+ EntityResponse with updated entity details
145
+
146
+ Raises:
147
+ ToolError: If the request fails
148
+ """
149
+ response = await call_put(
150
+ self.http_client,
151
+ f"{self._base_path}/entities/{entity_id}/move",
152
+ json={"destination_path": destination_path},
153
+ )
154
+ return EntityResponse.model_validate(response.json())
155
+
156
+ # --- Resolution ---
157
+
158
+ async def resolve_entity(self, identifier: str) -> str:
159
+ """Resolve a string identifier to an entity external_id.
160
+
161
+ Args:
162
+ identifier: The identifier to resolve (permalink, title, or path)
163
+
164
+ Returns:
165
+ The resolved entity external_id (UUID)
166
+
167
+ Raises:
168
+ ToolError: If the identifier cannot be resolved
169
+ """
170
+ response = await call_post(
171
+ self.http_client,
172
+ f"{self._base_path}/resolve",
173
+ json={"identifier": identifier},
174
+ )
175
+ data = response.json()
176
+ return data["external_id"]
@@ -0,0 +1,120 @@
1
+ """Typed client for memory/context API operations.
2
+
3
+ Encapsulates all /v2/projects/{project_id}/memory/* endpoints.
4
+ """
5
+
6
+ from typing import Optional
7
+
8
+ from httpx import AsyncClient
9
+
10
+ from basic_memory.mcp.tools.utils import call_get
11
+ from basic_memory.schemas.memory import GraphContext
12
+
13
+
14
+ class MemoryClient:
15
+ """Typed client for memory context operations.
16
+
17
+ Centralizes:
18
+ - API path construction for /v2/projects/{project_id}/memory/*
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 = MemoryClient(http_client, project_id)
25
+ context = await client.build_context("memory://specs/search")
26
+ """
27
+
28
+ def __init__(self, http_client: AsyncClient, project_id: str):
29
+ """Initialize the memory 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}/memory"
38
+
39
+ async def build_context(
40
+ self,
41
+ path: str,
42
+ *,
43
+ depth: int = 1,
44
+ timeframe: Optional[str] = None,
45
+ page: int = 1,
46
+ page_size: int = 10,
47
+ max_related: int = 10,
48
+ ) -> GraphContext:
49
+ """Build context from a memory path.
50
+
51
+ Args:
52
+ path: The path to build context for (without memory:// prefix)
53
+ depth: How deep to traverse relations
54
+ timeframe: Time filter (e.g., "7d", "1 week")
55
+ page: Page number (1-indexed)
56
+ page_size: Results per page
57
+ max_related: Maximum related items per result
58
+
59
+ Returns:
60
+ GraphContext with hierarchical results
61
+
62
+ Raises:
63
+ ToolError: If the request fails
64
+ """
65
+ params: dict = {
66
+ "depth": depth,
67
+ "page": page,
68
+ "page_size": page_size,
69
+ "max_related": max_related,
70
+ }
71
+ if timeframe:
72
+ params["timeframe"] = timeframe
73
+
74
+ response = await call_get(
75
+ self.http_client,
76
+ f"{self._base_path}/{path}",
77
+ params=params,
78
+ )
79
+ return GraphContext.model_validate(response.json())
80
+
81
+ async def recent(
82
+ self,
83
+ *,
84
+ timeframe: str = "7d",
85
+ depth: int = 1,
86
+ types: Optional[list[str]] = None,
87
+ page: int = 1,
88
+ page_size: int = 10,
89
+ ) -> GraphContext:
90
+ """Get recent activity.
91
+
92
+ Args:
93
+ timeframe: Time filter (e.g., "7d", "1 week", "2 days ago")
94
+ depth: How deep to traverse relations
95
+ types: Filter by item types
96
+ page: Page number (1-indexed)
97
+ page_size: Results per page
98
+
99
+ Returns:
100
+ GraphContext with recent activity
101
+
102
+ Raises:
103
+ ToolError: If the request fails
104
+ """
105
+ params: dict = {
106
+ "timeframe": timeframe,
107
+ "depth": depth,
108
+ "page": page,
109
+ "page_size": page_size,
110
+ }
111
+ if types:
112
+ # Join types as comma-separated string if provided
113
+ params["type"] = ",".join(types) if isinstance(types, list) else types
114
+
115
+ response = await call_get(
116
+ self.http_client,
117
+ f"{self._base_path}/recent",
118
+ params=params,
119
+ )
120
+ return GraphContext.model_validate(response.json())
@@ -0,0 +1,89 @@
1
+ """Typed client for project API operations.
2
+
3
+ Encapsulates project-level endpoints.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from httpx import AsyncClient
9
+
10
+ from basic_memory.mcp.tools.utils import call_get, call_post, call_delete
11
+ from basic_memory.schemas.project_info import ProjectList, ProjectStatusResponse
12
+
13
+
14
+ class ProjectClient:
15
+ """Typed client for project management operations.
16
+
17
+ Centralizes:
18
+ - API path construction for project endpoints
19
+ - Response validation via Pydantic models
20
+ - Consistent error handling through call_* utilities
21
+
22
+ Note: This client does not require a project_id since it operates
23
+ across projects.
24
+
25
+ Usage:
26
+ async with get_client() as http_client:
27
+ client = ProjectClient(http_client)
28
+ projects = await client.list_projects()
29
+ """
30
+
31
+ def __init__(self, http_client: AsyncClient):
32
+ """Initialize the project client.
33
+
34
+ Args:
35
+ http_client: HTTPX AsyncClient for making requests
36
+ """
37
+ self.http_client = http_client
38
+
39
+ async def list_projects(self) -> ProjectList:
40
+ """List all available projects.
41
+
42
+ Returns:
43
+ ProjectList with all projects and default project name
44
+
45
+ Raises:
46
+ ToolError: If the request fails
47
+ """
48
+ response = await call_get(
49
+ self.http_client,
50
+ "/projects/projects",
51
+ )
52
+ return ProjectList.model_validate(response.json())
53
+
54
+ async def create_project(self, project_data: dict[str, Any]) -> ProjectStatusResponse:
55
+ """Create a new project.
56
+
57
+ Args:
58
+ project_data: Project creation data (name, path, set_default)
59
+
60
+ Returns:
61
+ ProjectStatusResponse with creation result
62
+
63
+ Raises:
64
+ ToolError: If the request fails
65
+ """
66
+ response = await call_post(
67
+ self.http_client,
68
+ "/projects/projects",
69
+ json=project_data,
70
+ )
71
+ return ProjectStatusResponse.model_validate(response.json())
72
+
73
+ async def delete_project(self, project_external_id: str) -> ProjectStatusResponse:
74
+ """Delete a project by its external ID.
75
+
76
+ Args:
77
+ project_external_id: Project external ID (UUID)
78
+
79
+ Returns:
80
+ ProjectStatusResponse with deletion result
81
+
82
+ Raises:
83
+ ToolError: If the request fails
84
+ """
85
+ response = await call_delete(
86
+ self.http_client,
87
+ f"/v2/projects/{project_external_id}",
88
+ )
89
+ return ProjectStatusResponse.model_validate(response.json())
@@ -0,0 +1,71 @@
1
+ """Typed client for resource API operations.
2
+
3
+ Encapsulates all /v2/projects/{project_id}/resource/* endpoints.
4
+ """
5
+
6
+ from typing import Optional
7
+
8
+ from httpx import AsyncClient, Response
9
+
10
+ from basic_memory.mcp.tools.utils import call_get
11
+
12
+
13
+ class ResourceClient:
14
+ """Typed client for resource operations.
15
+
16
+ Centralizes:
17
+ - API path construction for /v2/projects/{project_id}/resource/*
18
+ - Consistent error handling through call_* utilities
19
+
20
+ Note: This client returns raw Response objects for resources since they
21
+ may be text, images, or other binary content that needs special handling.
22
+
23
+ Usage:
24
+ async with get_client() as http_client:
25
+ client = ResourceClient(http_client, project_id)
26
+ response = await client.read(entity_id)
27
+ text = response.text
28
+ """
29
+
30
+ def __init__(self, http_client: AsyncClient, project_id: str):
31
+ """Initialize the resource client.
32
+
33
+ Args:
34
+ http_client: HTTPX AsyncClient for making requests
35
+ project_id: Project external_id (UUID) for API calls
36
+ """
37
+ self.http_client = http_client
38
+ self.project_id = project_id
39
+ self._base_path = f"/v2/projects/{project_id}/resource"
40
+
41
+ async def read(
42
+ self,
43
+ entity_id: str,
44
+ *,
45
+ page: Optional[int] = None,
46
+ page_size: Optional[int] = None,
47
+ ) -> Response:
48
+ """Read a resource by entity ID.
49
+
50
+ Args:
51
+ entity_id: Entity external_id (UUID)
52
+ page: Optional page number for paginated content
53
+ page_size: Optional page size for paginated content
54
+
55
+ Returns:
56
+ Raw HTTP Response (caller handles text/binary content)
57
+
58
+ Raises:
59
+ ToolError: If the resource is not found or request fails
60
+ """
61
+ params: dict = {}
62
+ if page is not None:
63
+ params["page"] = page
64
+ if page_size is not None:
65
+ params["page_size"] = page_size
66
+
67
+ return await call_get(
68
+ self.http_client,
69
+ f"{self._base_path}/{entity_id}",
70
+ params=params if params else None,
71
+ )