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
@@ -0,0 +1,286 @@
1
+ """V2 Resource Router - ID-based resource content operations.
2
+
3
+ This router uses entity external_ids (UUIDs) for all operations, with file paths
4
+ in request bodies when needed. This is consistent with v2's external_id-first design.
5
+
6
+ Key differences from v1:
7
+ - Uses UUID external_ids in URL paths instead of integer IDs or file paths
8
+ - File paths are in request bodies for create/update operations
9
+ - More RESTful: POST for create, PUT for update, GET for read
10
+ """
11
+
12
+ from pathlib import Path as PathLib
13
+
14
+ from fastapi import APIRouter, HTTPException, Response, Path
15
+ from loguru import logger
16
+
17
+ from basic_memory.deps import (
18
+ ProjectConfigV2ExternalDep,
19
+ FileServiceV2ExternalDep,
20
+ EntityRepositoryV2ExternalDep,
21
+ SearchServiceV2ExternalDep,
22
+ )
23
+ from basic_memory.models.knowledge import Entity as EntityModel
24
+ from basic_memory.schemas.v2.resource import (
25
+ CreateResourceRequest,
26
+ UpdateResourceRequest,
27
+ ResourceResponse,
28
+ )
29
+ from basic_memory.utils import validate_project_path
30
+
31
+ router = APIRouter(prefix="/resource", tags=["resources-v2"])
32
+
33
+
34
+ @router.get("/{entity_id}")
35
+ async def get_resource_content(
36
+ config: ProjectConfigV2ExternalDep,
37
+ entity_repository: EntityRepositoryV2ExternalDep,
38
+ file_service: FileServiceV2ExternalDep,
39
+ project_id: str = Path(..., description="Project external UUID"),
40
+ entity_id: str = Path(..., description="Entity external UUID"),
41
+ ) -> Response:
42
+ """Get raw resource content by entity external_id.
43
+
44
+ Args:
45
+ project_id: Project external UUID from URL path
46
+ entity_id: Entity external UUID
47
+ config: Project configuration
48
+ entity_repository: Entity repository for fetching entity data
49
+ file_service: File service for reading file content
50
+
51
+ Returns:
52
+ Response with entity content
53
+
54
+ Raises:
55
+ HTTPException: 404 if entity or file not found
56
+ """
57
+ logger.debug(f"V2 Getting content for project {project_id}, entity_id: {entity_id}")
58
+
59
+ # Get entity by external_id
60
+ entity = await entity_repository.get_by_external_id(entity_id)
61
+ if not entity:
62
+ raise HTTPException(status_code=404, detail=f"Entity {entity_id} not found")
63
+
64
+ # Validate entity file path to prevent path traversal
65
+ project_path = PathLib(config.home)
66
+ if not validate_project_path(entity.file_path, project_path):
67
+ logger.error( # pragma: no cover
68
+ f"Invalid file path in entity {entity.id}: {entity.file_path}"
69
+ )
70
+ raise HTTPException( # pragma: no cover
71
+ status_code=500,
72
+ detail="Entity contains invalid file path",
73
+ )
74
+
75
+ # Check file exists via file_service (for cloud compatibility)
76
+ if not await file_service.exists(entity.file_path):
77
+ raise HTTPException( # pragma: no cover
78
+ status_code=404,
79
+ detail=f"File not found: {entity.file_path}",
80
+ )
81
+
82
+ # Read content via file_service as bytes (works with both local and S3)
83
+ content = await file_service.read_file_bytes(entity.file_path)
84
+ content_type = file_service.content_type(entity.file_path)
85
+
86
+ return Response(content=content, media_type=content_type)
87
+
88
+
89
+ @router.post("", response_model=ResourceResponse)
90
+ async def create_resource(
91
+ data: CreateResourceRequest,
92
+ config: ProjectConfigV2ExternalDep,
93
+ file_service: FileServiceV2ExternalDep,
94
+ entity_repository: EntityRepositoryV2ExternalDep,
95
+ search_service: SearchServiceV2ExternalDep,
96
+ project_id: str = Path(..., description="Project external UUID"),
97
+ ) -> ResourceResponse:
98
+ """Create a new resource file.
99
+
100
+ Args:
101
+ project_id: Project external UUID from URL path
102
+ data: Create resource request with file_path and content
103
+ config: Project configuration
104
+ file_service: File service for writing files
105
+ entity_repository: Entity repository for creating entities
106
+ search_service: Search service for indexing
107
+
108
+ Returns:
109
+ ResourceResponse with file information including entity_id and external_id
110
+
111
+ Raises:
112
+ HTTPException: 400 for invalid file paths, 409 if file already exists
113
+ """
114
+ try:
115
+ # Validate path to prevent path traversal attacks
116
+ project_path = PathLib(config.home)
117
+ if not validate_project_path(data.file_path, project_path):
118
+ logger.warning(
119
+ f"Invalid file path attempted: {data.file_path} in project {config.name}"
120
+ )
121
+ raise HTTPException(
122
+ status_code=400,
123
+ detail=f"Invalid file path: {data.file_path}. "
124
+ "Path must be relative and stay within project boundaries.",
125
+ )
126
+
127
+ # Check if entity already exists
128
+ existing_entity = await entity_repository.get_by_file_path(data.file_path)
129
+ if existing_entity:
130
+ raise HTTPException(
131
+ status_code=409,
132
+ detail=f"Resource already exists at {data.file_path} with entity_id {existing_entity.external_id}. "
133
+ f"Use PUT /resource/{existing_entity.external_id} to update it.",
134
+ )
135
+
136
+ # Cloud compatibility: avoid assuming a local filesystem path.
137
+ # Delegate directory creation + writes to FileService (local or S3).
138
+ await file_service.ensure_directory(PathLib(data.file_path).parent)
139
+ checksum = await file_service.write_file(data.file_path, data.content)
140
+
141
+ # Get file info
142
+ file_metadata = await file_service.get_file_metadata(data.file_path)
143
+
144
+ # Determine file details
145
+ file_name = PathLib(data.file_path).name
146
+ content_type = file_service.content_type(data.file_path)
147
+ entity_type = "canvas" if data.file_path.endswith(".canvas") else "file"
148
+
149
+ # Create a new entity model
150
+ entity = EntityModel(
151
+ title=file_name,
152
+ entity_type=entity_type,
153
+ content_type=content_type,
154
+ file_path=data.file_path,
155
+ checksum=checksum,
156
+ created_at=file_metadata.created_at,
157
+ updated_at=file_metadata.modified_at,
158
+ )
159
+ entity = await entity_repository.add(entity)
160
+
161
+ # Index the file for search
162
+ await search_service.index_entity(entity) # pyright: ignore
163
+
164
+ # Return success response
165
+ return ResourceResponse(
166
+ entity_id=entity.id,
167
+ external_id=entity.external_id,
168
+ file_path=data.file_path,
169
+ checksum=checksum,
170
+ size=file_metadata.size,
171
+ created_at=file_metadata.created_at.timestamp(),
172
+ modified_at=file_metadata.modified_at.timestamp(),
173
+ )
174
+ except HTTPException:
175
+ # Re-raise HTTP exceptions without wrapping
176
+ raise
177
+ except Exception as e: # pragma: no cover
178
+ logger.error(f"Error creating resource {data.file_path}: {e}")
179
+ raise HTTPException(status_code=500, detail=f"Failed to create resource: {str(e)}")
180
+
181
+
182
+ @router.put("/{entity_id}", response_model=ResourceResponse)
183
+ async def update_resource(
184
+ data: UpdateResourceRequest,
185
+ config: ProjectConfigV2ExternalDep,
186
+ file_service: FileServiceV2ExternalDep,
187
+ entity_repository: EntityRepositoryV2ExternalDep,
188
+ search_service: SearchServiceV2ExternalDep,
189
+ project_id: str = Path(..., description="Project external UUID"),
190
+ entity_id: str = Path(..., description="Entity external UUID"),
191
+ ) -> ResourceResponse:
192
+ """Update an existing resource by entity external_id.
193
+
194
+ Can update content and optionally move the file to a new path.
195
+
196
+ Args:
197
+ project_id: Project external UUID from URL path
198
+ entity_id: Entity external UUID of the resource to update
199
+ data: Update resource request with content and optional new file_path
200
+ config: Project configuration
201
+ file_service: File service for writing files
202
+ entity_repository: Entity repository for updating entities
203
+ search_service: Search service for indexing
204
+
205
+ Returns:
206
+ ResourceResponse with updated file information
207
+
208
+ Raises:
209
+ HTTPException: 404 if entity not found, 400 for invalid paths
210
+ """
211
+ try:
212
+ # Get existing entity by external_id
213
+ entity = await entity_repository.get_by_external_id(entity_id)
214
+ if not entity:
215
+ raise HTTPException(status_code=404, detail=f"Entity {entity_id} not found")
216
+
217
+ # Determine target file path
218
+ target_file_path = data.file_path if data.file_path else entity.file_path
219
+
220
+ # Validate path to prevent path traversal attacks
221
+ project_path = PathLib(config.home)
222
+ if not validate_project_path(target_file_path, project_path):
223
+ logger.warning(
224
+ f"Invalid file path attempted: {target_file_path} in project {config.name}"
225
+ )
226
+ raise HTTPException(
227
+ status_code=400,
228
+ detail=f"Invalid file path: {target_file_path}. "
229
+ "Path must be relative and stay within project boundaries.",
230
+ )
231
+
232
+ # If moving file, handle the move
233
+ if data.file_path and data.file_path != entity.file_path:
234
+ # Ensure new parent directory exists (no-op for S3)
235
+ await file_service.ensure_directory(PathLib(target_file_path).parent)
236
+
237
+ # If old file exists, remove it via file_service (for cloud compatibility)
238
+ if await file_service.exists(entity.file_path):
239
+ await file_service.delete_file(entity.file_path)
240
+ else:
241
+ # Ensure directory exists for in-place update
242
+ await file_service.ensure_directory(PathLib(target_file_path).parent)
243
+
244
+ # Write content to target file
245
+ checksum = await file_service.write_file(target_file_path, data.content)
246
+
247
+ # Get file info
248
+ file_metadata = await file_service.get_file_metadata(target_file_path)
249
+
250
+ # Determine file details
251
+ file_name = PathLib(target_file_path).name
252
+ content_type = file_service.content_type(target_file_path)
253
+ entity_type = "canvas" if target_file_path.endswith(".canvas") else "file"
254
+
255
+ # Update entity using internal ID
256
+ updated_entity = await entity_repository.update(
257
+ entity.id,
258
+ {
259
+ "title": file_name,
260
+ "entity_type": entity_type,
261
+ "content_type": content_type,
262
+ "file_path": target_file_path,
263
+ "checksum": checksum,
264
+ "updated_at": file_metadata.modified_at,
265
+ },
266
+ )
267
+
268
+ # Index the updated file for search
269
+ await search_service.index_entity(updated_entity) # pyright: ignore
270
+
271
+ # Return success response
272
+ return ResourceResponse(
273
+ entity_id=entity.id,
274
+ external_id=entity.external_id,
275
+ file_path=target_file_path,
276
+ checksum=checksum,
277
+ size=file_metadata.size,
278
+ created_at=file_metadata.created_at.timestamp(),
279
+ modified_at=file_metadata.modified_at.timestamp(),
280
+ )
281
+ except HTTPException:
282
+ # Re-raise HTTP exceptions without wrapping
283
+ raise
284
+ except Exception as e: # pragma: no cover
285
+ logger.error(f"Error updating resource {entity_id}: {e}")
286
+ raise HTTPException(status_code=500, detail=f"Failed to update resource: {str(e)}")
@@ -0,0 +1,73 @@
1
+ """V2 router for search operations.
2
+
3
+ This router uses external_id UUIDs for stable, API-friendly routing.
4
+ V1 uses string-based project names which are less efficient and less stable.
5
+ """
6
+
7
+ from fastapi import APIRouter, BackgroundTasks, Path
8
+
9
+ from basic_memory.api.routers.utils import to_search_results
10
+ from basic_memory.schemas.search import SearchQuery, SearchResponse
11
+ from basic_memory.deps import SearchServiceV2ExternalDep, EntityServiceV2ExternalDep
12
+
13
+ # Note: No prefix here - it's added during registration as /v2/{project_id}/search
14
+ router = APIRouter(tags=["search"])
15
+
16
+
17
+ @router.post("/search/", response_model=SearchResponse)
18
+ async def search(
19
+ query: SearchQuery,
20
+ search_service: SearchServiceV2ExternalDep,
21
+ entity_service: EntityServiceV2ExternalDep,
22
+ project_id: str = Path(..., description="Project external UUID"),
23
+ page: int = 1,
24
+ page_size: int = 10,
25
+ ):
26
+ """Search across all knowledge and documents in a project.
27
+
28
+ V2 uses external_id UUIDs for stable API references.
29
+
30
+ Args:
31
+ project_id: Project external UUID from URL path
32
+ query: Search query parameters (text, filters, etc.)
33
+ search_service: Search service scoped to project
34
+ entity_service: Entity service scoped to project
35
+ page: Page number for pagination
36
+ page_size: Number of results per page
37
+
38
+ Returns:
39
+ SearchResponse with paginated search results
40
+ """
41
+ limit = page_size
42
+ offset = (page - 1) * page_size
43
+ results = await search_service.search(query, limit=limit, offset=offset)
44
+ search_results = await to_search_results(entity_service, results)
45
+ return SearchResponse(
46
+ results=search_results,
47
+ current_page=page,
48
+ page_size=page_size,
49
+ )
50
+
51
+
52
+ @router.post("/search/reindex")
53
+ async def reindex(
54
+ background_tasks: BackgroundTasks,
55
+ search_service: SearchServiceV2ExternalDep,
56
+ project_id: str = Path(..., description="Project external UUID"),
57
+ ):
58
+ """Recreate and populate the search index for a project.
59
+
60
+ This is a background operation that rebuilds the search index
61
+ from scratch. Useful after bulk updates or if the index becomes
62
+ corrupted.
63
+
64
+ Args:
65
+ project_id: Project external UUID from URL path
66
+ background_tasks: FastAPI background tasks handler
67
+ search_service: Search service scoped to project
68
+
69
+ Returns:
70
+ Status message indicating reindex has been initiated
71
+ """
72
+ await search_service.reindex_all(background_tasks=background_tasks)
73
+ return {"status": "ok", "message": "Reindex initiated"}
basic_memory/cli/app.py CHANGED
@@ -1,20 +1,90 @@
1
- import asyncio
1
+ # Suppress Logfire "not configured" warning - we only use Logfire in cloud/server contexts
2
+ import os
2
3
 
3
- import typer
4
+ os.environ.setdefault("LOGFIRE_IGNORE_NO_CONFIG", "1")
4
5
 
5
- from basic_memory import db
6
- from basic_memory.config import config
7
- from basic_memory.utils import setup_logging
6
+ # Remove loguru's default handler IMMEDIATELY, before any other imports.
7
+ # This prevents DEBUG logs from appearing on stdout during module-level
8
+ # initialization (e.g., template_loader.TemplateLoader() logs at DEBUG level).
9
+ from loguru import logger
8
10
 
9
- setup_logging(log_file=".basic-memory/basic-memory-cli.log", console=False) # pragma: no cover
11
+ logger.remove()
12
+
13
+ from typing import Optional # noqa: E402
14
+
15
+ import typer # noqa: E402
16
+
17
+ from basic_memory.cli.container import CliContainer, set_container # noqa: E402
18
+ from basic_memory.config import init_cli_logging # noqa: E402
19
+ from basic_memory.telemetry import show_notice_if_needed, track_app_started # noqa: E402
20
+
21
+
22
+ def version_callback(value: bool) -> None:
23
+ """Show version and exit."""
24
+ if value: # pragma: no cover
25
+ import basic_memory
26
+
27
+ typer.echo(f"Basic Memory version: {basic_memory.__version__}")
28
+ raise typer.Exit()
10
29
 
11
- asyncio.run(db.run_migrations(config))
12
30
 
13
31
  app = typer.Typer(name="basic-memory")
14
32
 
15
- import_app = typer.Typer()
16
- app.add_typer(import_app, name="import")
17
33
 
34
+ @app.callback()
35
+ def app_callback(
36
+ ctx: typer.Context,
37
+ version: Optional[bool] = typer.Option(
38
+ None,
39
+ "--version",
40
+ "-v",
41
+ help="Show version and exit.",
42
+ callback=version_callback,
43
+ is_eager=True,
44
+ ),
45
+ ) -> None:
46
+ """Basic Memory - Local-first personal knowledge management."""
47
+
48
+ # Initialize logging for CLI (file only, no stdout)
49
+ init_cli_logging()
50
+
51
+ # --- Composition Root ---
52
+ # Create container and read config (single point of config access)
53
+ container = CliContainer.create()
54
+ set_container(container)
18
55
 
19
- claude_app = typer.Typer()
56
+ # Show telemetry notice and track CLI startup
57
+ # Skip for 'mcp' command - it handles its own telemetry in lifespan
58
+ # Skip for 'telemetry' command - avoid issues when user is managing telemetry
59
+ if ctx.invoked_subcommand not in {"mcp", "telemetry"}:
60
+ show_notice_if_needed()
61
+ track_app_started("cli")
62
+
63
+ # Run initialization for commands that don't use the API
64
+ # Skip for 'mcp' command - it has its own lifespan that handles initialization
65
+ # Skip for API-using commands (status, sync, etc.) - they handle initialization via deps.py
66
+ # Skip for 'reset' command - it manages its own database lifecycle
67
+ skip_init_commands = {"mcp", "status", "sync", "project", "tool", "reset"}
68
+ if (
69
+ not version
70
+ and ctx.invoked_subcommand is not None
71
+ and ctx.invoked_subcommand not in skip_init_commands
72
+ ):
73
+ from basic_memory.services.initialization import ensure_initialization
74
+
75
+ ensure_initialization(container.config)
76
+
77
+
78
+ ## import
79
+ # Register sub-command groups
80
+ import_app = typer.Typer(help="Import data from various sources")
81
+ app.add_typer(import_app, name="import")
82
+
83
+ claude_app = typer.Typer(help="Import Conversations from Claude JSON export.")
20
84
  import_app.add_typer(claude_app, name="claude")
85
+
86
+
87
+ ## cloud
88
+
89
+ cloud_app = typer.Typer(help="Access Basic Memory Cloud")
90
+ app.add_typer(cloud_app, name="cloud")