basic-memory 0.14.4__py3-none-any.whl → 0.15.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.

Potentially problematic release.


This version of basic-memory might be problematic. Click here for more details.

Files changed (84) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +5 -9
  3. basic_memory/api/app.py +10 -4
  4. basic_memory/api/routers/directory_router.py +23 -2
  5. basic_memory/api/routers/knowledge_router.py +25 -8
  6. basic_memory/api/routers/project_router.py +100 -4
  7. basic_memory/cli/app.py +9 -28
  8. basic_memory/cli/auth.py +277 -0
  9. basic_memory/cli/commands/cloud/__init__.py +5 -0
  10. basic_memory/cli/commands/cloud/api_client.py +112 -0
  11. basic_memory/cli/commands/cloud/bisync_commands.py +818 -0
  12. basic_memory/cli/commands/cloud/core_commands.py +288 -0
  13. basic_memory/cli/commands/cloud/mount_commands.py +295 -0
  14. basic_memory/cli/commands/cloud/rclone_config.py +288 -0
  15. basic_memory/cli/commands/cloud/rclone_installer.py +198 -0
  16. basic_memory/cli/commands/command_utils.py +43 -0
  17. basic_memory/cli/commands/import_memory_json.py +0 -4
  18. basic_memory/cli/commands/mcp.py +77 -60
  19. basic_memory/cli/commands/project.py +154 -152
  20. basic_memory/cli/commands/status.py +25 -22
  21. basic_memory/cli/commands/sync.py +45 -228
  22. basic_memory/cli/commands/tool.py +87 -16
  23. basic_memory/cli/main.py +1 -0
  24. basic_memory/config.py +131 -21
  25. basic_memory/db.py +104 -3
  26. basic_memory/deps.py +27 -8
  27. basic_memory/file_utils.py +37 -13
  28. basic_memory/ignore_utils.py +295 -0
  29. basic_memory/markdown/plugins.py +9 -7
  30. basic_memory/mcp/async_client.py +124 -14
  31. basic_memory/mcp/project_context.py +141 -0
  32. basic_memory/mcp/prompts/ai_assistant_guide.py +49 -4
  33. basic_memory/mcp/prompts/continue_conversation.py +17 -16
  34. basic_memory/mcp/prompts/recent_activity.py +116 -32
  35. basic_memory/mcp/prompts/search.py +13 -12
  36. basic_memory/mcp/prompts/utils.py +11 -4
  37. basic_memory/mcp/resources/ai_assistant_guide.md +211 -341
  38. basic_memory/mcp/resources/project_info.py +27 -11
  39. basic_memory/mcp/server.py +0 -37
  40. basic_memory/mcp/tools/__init__.py +5 -6
  41. basic_memory/mcp/tools/build_context.py +67 -56
  42. basic_memory/mcp/tools/canvas.py +38 -26
  43. basic_memory/mcp/tools/chatgpt_tools.py +187 -0
  44. basic_memory/mcp/tools/delete_note.py +81 -47
  45. basic_memory/mcp/tools/edit_note.py +155 -138
  46. basic_memory/mcp/tools/list_directory.py +112 -99
  47. basic_memory/mcp/tools/move_note.py +181 -101
  48. basic_memory/mcp/tools/project_management.py +113 -277
  49. basic_memory/mcp/tools/read_content.py +91 -74
  50. basic_memory/mcp/tools/read_note.py +152 -115
  51. basic_memory/mcp/tools/recent_activity.py +471 -68
  52. basic_memory/mcp/tools/search.py +105 -92
  53. basic_memory/mcp/tools/sync_status.py +136 -130
  54. basic_memory/mcp/tools/utils.py +4 -0
  55. basic_memory/mcp/tools/view_note.py +44 -33
  56. basic_memory/mcp/tools/write_note.py +151 -90
  57. basic_memory/models/knowledge.py +12 -6
  58. basic_memory/models/project.py +6 -2
  59. basic_memory/repository/entity_repository.py +89 -82
  60. basic_memory/repository/relation_repository.py +13 -0
  61. basic_memory/repository/repository.py +18 -5
  62. basic_memory/repository/search_repository.py +46 -2
  63. basic_memory/schemas/__init__.py +6 -0
  64. basic_memory/schemas/base.py +39 -11
  65. basic_memory/schemas/cloud.py +46 -0
  66. basic_memory/schemas/memory.py +90 -21
  67. basic_memory/schemas/project_info.py +9 -10
  68. basic_memory/schemas/sync_report.py +48 -0
  69. basic_memory/services/context_service.py +25 -11
  70. basic_memory/services/directory_service.py +124 -3
  71. basic_memory/services/entity_service.py +100 -48
  72. basic_memory/services/initialization.py +30 -11
  73. basic_memory/services/project_service.py +101 -24
  74. basic_memory/services/search_service.py +16 -8
  75. basic_memory/sync/sync_service.py +173 -34
  76. basic_memory/sync/watch_service.py +101 -40
  77. basic_memory/utils.py +14 -4
  78. {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/METADATA +57 -9
  79. basic_memory-0.15.1.dist-info/RECORD +146 -0
  80. basic_memory/mcp/project_session.py +0 -120
  81. basic_memory-0.14.4.dist-info/RECORD +0 -133
  82. {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/WHEEL +0 -0
  83. {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/entry_points.txt +0 -0
  84. {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -4,15 +4,18 @@ from textwrap import dedent
4
4
  from typing import List, Optional
5
5
 
6
6
  from loguru import logger
7
+ from fastmcp import Context
7
8
 
8
- from basic_memory.mcp.async_client import client
9
+ from basic_memory.mcp.async_client import get_client
10
+ from basic_memory.mcp.project_context import get_active_project
9
11
  from basic_memory.mcp.server import mcp
10
12
  from basic_memory.mcp.tools.utils import call_post
11
- from basic_memory.mcp.project_session import get_active_project
12
13
  from basic_memory.schemas.search import SearchItemType, SearchQuery, SearchResponse
13
14
 
14
15
 
15
- def _format_search_error_response(error_message: str, query: str, search_type: str = "text") -> str:
16
+ def _format_search_error_response(
17
+ project: str, error_message: str, query: str, search_type: str = "text"
18
+ ) -> str:
16
19
  """Format helpful error responses for search failures that guide users to successful searches."""
17
20
 
18
21
  # FTS5 syntax errors
@@ -50,13 +53,13 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
50
53
 
51
54
  ## Try again with:
52
55
  ```
53
- search_notes("{clean_query}")
56
+ search_notes("{project}","{clean_query}")
54
57
  ```
55
58
 
56
59
  ## Alternative search strategies:
57
- - Break into simpler terms: `search_notes("{" ".join(clean_query.split()[:2])}")`
58
- - Try different search types: `search_notes("{clean_query}", search_type="title")`
59
- - Use filtering: `search_notes("{clean_query}", types=["entity"])`
60
+ - Break into simpler terms: `search_notes("{project}", "{" ".join(clean_query.split()[:2])}")`
61
+ - Try different search types: `search_notes("{project}","{clean_query}", search_type="title")`
62
+ - Use filtering: `search_notes("{project}","{clean_query}", types=["entity"])`
60
63
  """).strip()
61
64
 
62
65
  # Project not found errors (check before general "not found")
@@ -68,11 +71,9 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
68
71
 
69
72
  ## How to resolve:
70
73
  1. **Check available projects**: `list_projects()`
71
- 2. **Switch to valid project**: `switch_project("valid-project-name")`
72
74
  3. **Verify project setup**: Ensure your project is properly configured
73
75
 
74
76
  ## Current session info:
75
- - Check current project: `get_current_project()`
76
77
  - See available projects: `list_projects()`
77
78
  """).strip()
78
79
 
@@ -100,29 +101,28 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
100
101
  - Try synonyms or related terms
101
102
 
102
103
  3. **Use different search approaches**:
103
- - **Text search**: `search_notes("{query}", search_type="text")` (searches full content)
104
- - **Title search**: `search_notes("{query}", search_type="title")` (searches only titles)
105
- - **Permalink search**: `search_notes("{query}", search_type="permalink")` (searches file paths)
104
+ - **Text search**: `search_notes("{project}","{query}", search_type="text")` (searches full content)
105
+ - **Title search**: `search_notes("{project}","{query}", search_type="title")` (searches only titles)
106
+ - **Permalink search**: `search_notes("{project}","{query}", search_type="permalink")` (searches file paths)
106
107
 
107
108
  4. **Try boolean operators for broader results**:
108
- - OR search: `search_notes("{" OR ".join(query.split()[:3])}")`
109
+ - OR search: `search_notes("{project}","{" OR ".join(query.split()[:3])}")`
109
110
  - Remove restrictive terms: Focus on the most important keywords
110
111
 
111
112
  5. **Use filtering to narrow scope**:
112
- - By content type: `search_notes("{query}", types=["entity"])`
113
- - By recent content: `search_notes("{query}", after_date="1 week")`
114
- - By entity type: `search_notes("{query}", entity_types=["observation"])`
113
+ - By content type: `search_notes("{project}","{query}", types=["entity"])`
114
+ - By recent content: `search_notes("{project}","{query}", after_date="1 week")`
115
+ - By entity type: `search_notes("{project}","{query}", entity_types=["observation"])`
115
116
 
116
117
  6. **Try advanced search patterns**:
117
- - Tag search: `search_notes("tag:your-tag")`
118
- - Category search: `search_notes("category:observation")`
119
- - Pattern matching: `search_notes("*{query}*", search_type="permalink")`
118
+ - Tag search: `search_notes("{project}","tag:your-tag")`
119
+ - Category search: `search_notes("{project}","category:observation")`
120
+ - Pattern matching: `search_notes("{project}","*{query}*", search_type="permalink")`
120
121
 
121
122
  ## Explore what content exists:
122
123
  - **Recent activity**: `recent_activity(timeframe="7d")` - See what's been updated recently
123
- - **List directories**: `list_directory("/")` - Browse all content
124
- - **Browse by folder**: `list_directory("/notes")` or `list_directory("/docs")`
125
- - **Check project**: `get_current_project()` - Verify you're in the right project
124
+ - **List directories**: `list_directory("{project}","/")` - Browse all content
125
+ - **Browse by folder**: `list_directory("{project}","/notes")` or `list_directory("/docs")`
126
126
  """).strip()
127
127
 
128
128
  # Server/API errors
@@ -138,9 +138,9 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
138
138
  3. **Check project status**: Ensure your project is properly synced
139
139
 
140
140
  ## Alternative approaches:
141
- - Browse files directly: `list_directory("/")`
141
+ - Browse files directly: `list_directory("{project}","/")`
142
142
  - Check recent activity: `recent_activity(timeframe="7d")`
143
- - Try a different search type: `search_notes("{query}", search_type="title")`
143
+ - Try a different search type: `search_notes("{project}","{query}", search_type="title")`
144
144
 
145
145
  ## If the problem persists:
146
146
  The search index might need to be rebuilt. Send a message to support@basicmachines.co or check the project sync status.
@@ -162,9 +162,7 @@ You don't have permission to search in the current project: {error_message}
162
162
  3. **Check authentication**: You might need to re-authenticate
163
163
 
164
164
  ## Alternative actions:
165
- - List available projects: `list_projects()`
166
- - Switch to accessible project: `switch_project("project-name")`
167
- - Check current project: `get_current_project()`"""
165
+ - List available projects: `list_projects()`"""
168
166
 
169
167
  # Generic fallback
170
168
  return f"""# Search Failed
@@ -179,17 +177,16 @@ Error searching for '{query}': {error_message}
179
177
 
180
178
  ## Alternative search approaches:
181
179
  - **Different search types**:
182
- - Title only: `search_notes("{query}", search_type="title")`
183
- - Permalink patterns: `search_notes("{query}*", search_type="permalink")`
184
- - **With filters**: `search_notes("{query}", types=["entity"])`
185
- - **Recent content**: `search_notes("{query}", after_date="1 week")`
186
- - **Boolean variations**: `search_notes("{" OR ".join(query.split()[:2])}")`
180
+ - Title only: `search_notes("{project}","{query}", search_type="title")`
181
+ - Permalink patterns: `search_notes("{project}","{query}*", search_type="permalink")`
182
+ - **With filters**: `search_notes("{project}","{query}", types=["entity"])`
183
+ - **Recent content**: `search_notes("{project}","{query}", after_date="1 week")`
184
+ - **Boolean variations**: `search_notes("{project}","{" OR ".join(query.split()[:2])}")`
187
185
 
188
186
  ## Explore your content:
189
- - **Browse files**: `list_directory("/")` - See all available content
187
+ - **Browse files**: `list_directory("{project}","/")` - See all available content
190
188
  - **Recent activity**: `recent_activity(timeframe="7d")` - Check what's been updated
191
- - **Project info**: `get_current_project()` - Verify current project
192
- - **All projects**: `list_projects()` - Switch to different project if needed
189
+ - **All projects**: `list_projects()`
193
190
 
194
191
  ## Search syntax reference:
195
192
  - **Basic**: `keyword` or `multiple words`
@@ -204,13 +201,14 @@ Error searching for '{query}': {error_message}
204
201
  )
205
202
  async def search_notes(
206
203
  query: str,
204
+ project: Optional[str] = None,
207
205
  page: int = 1,
208
206
  page_size: int = 10,
209
207
  search_type: str = "text",
210
208
  types: Optional[List[str]] = None,
211
209
  entity_types: Optional[List[str]] = None,
212
210
  after_date: Optional[str] = None,
213
- project: Optional[str] = None,
211
+ context: Context | None = None,
214
212
  ) -> SearchResponse | str:
215
213
  """Search across all content in the knowledge base with comprehensive syntax support.
216
214
 
@@ -218,51 +216,57 @@ async def search_notes(
218
216
  or exact permalink lookup. It supports filtering by content type, entity type,
219
217
  and date, with advanced boolean and phrase search capabilities.
220
218
 
219
+ Project Resolution:
220
+ Server resolves projects in this order: Single Project Mode → project parameter → default project.
221
+ If project unknown, use list_memory_projects() or recent_activity() first.
222
+
221
223
  ## Search Syntax Examples
222
224
 
223
225
  ### Basic Searches
224
- - `search_notes("keyword")` - Find any content containing "keyword"
225
- - `search_notes("exact phrase")` - Search for exact phrase match
226
+ - `search_notes("my-project", "keyword")` - Find any content containing "keyword"
227
+ - `search_notes("work-docs", "'exact phrase'")` - Search for exact phrase match
226
228
 
227
229
  ### Advanced Boolean Searches
228
- - `search_notes("term1 term2")` - Find content with both terms (implicit AND)
229
- - `search_notes("term1 AND term2")` - Explicit AND search (both terms required)
230
- - `search_notes("term1 OR term2")` - Either term can be present
231
- - `search_notes("term1 NOT term2")` - Include term1 but exclude term2
232
- - `search_notes("(project OR planning) AND notes")` - Grouped boolean logic
230
+ - `search_notes("my-project", "term1 term2")` - Find content with both terms (implicit AND)
231
+ - `search_notes("my-project", "term1 AND term2")` - Explicit AND search (both terms required)
232
+ - `search_notes("my-project", "term1 OR term2")` - Either term can be present
233
+ - `search_notes("my-project", "term1 NOT term2")` - Include term1 but exclude term2
234
+ - `search_notes("my-project", "(project OR planning) AND notes")` - Grouped boolean logic
233
235
 
234
236
  ### Content-Specific Searches
235
- - `search_notes("tag:example")` - Search within specific tags (if supported by content)
236
- - `search_notes("category:observation")` - Filter by observation categories
237
- - `search_notes("author:username")` - Find content by author (if metadata available)
237
+ - `search_notes("research", "tag:example")` - Search within specific tags (if supported by content)
238
+ - `search_notes("work-project", "category:observation")` - Filter by observation categories
239
+ - `search_notes("team-docs", "author:username")` - Find content by author (if metadata available)
238
240
 
239
241
  ### Search Type Examples
240
- - `search_notes("Meeting", search_type="title")` - Search only in titles
241
- - `search_notes("docs/meeting-*", search_type="permalink")` - Pattern match permalinks
242
- - `search_notes("keyword", search_type="text")` - Full-text search (default)
242
+ - `search_notes("my-project", "Meeting", search_type="title")` - Search only in titles
243
+ - `search_notes("work-docs", "docs/meeting-*", search_type="permalink")` - Pattern match permalinks
244
+ - `search_notes("research", "keyword", search_type="text")` - Full-text search (default)
243
245
 
244
246
  ### Filtering Options
245
- - `search_notes("query", types=["entity"])` - Search only entities
246
- - `search_notes("query", types=["note", "person"])` - Multiple content types
247
- - `search_notes("query", entity_types=["observation"])` - Filter by entity type
248
- - `search_notes("query", after_date="2024-01-01")` - Recent content only
249
- - `search_notes("query", after_date="1 week")` - Relative date filtering
247
+ - `search_notes("my-project", "query", types=["entity"])` - Search only entities
248
+ - `search_notes("work-docs", "query", types=["note", "person"])` - Multiple content types
249
+ - `search_notes("research", "query", entity_types=["observation"])` - Filter by entity type
250
+ - `search_notes("team-docs", "query", after_date="2024-01-01")` - Recent content only
251
+ - `search_notes("my-project", "query", after_date="1 week")` - Relative date filtering
250
252
 
251
253
  ### Advanced Pattern Examples
252
- - `search_notes("project AND (meeting OR discussion)")` - Complex boolean logic
253
- - `search_notes("\"exact phrase\" AND keyword")` - Combine phrase and keyword search
254
- - `search_notes("bug NOT fixed")` - Exclude resolved issues
255
- - `search_notes("docs/2024-*", search_type="permalink")` - Year-based permalink search
254
+ - `search_notes("work-project", "project AND (meeting OR discussion)")` - Complex boolean logic
255
+ - `search_notes("research", "\"exact phrase\" AND keyword")` - Combine phrase and keyword search
256
+ - `search_notes("dev-notes", "bug NOT fixed")` - Exclude resolved issues
257
+ - `search_notes("archive", "docs/2024-*", search_type="permalink")` - Year-based permalink search
256
258
 
257
259
  Args:
258
260
  query: The search query string (supports boolean operators, phrases, patterns)
261
+ project: Project name to search in. Optional - server will resolve using hierarchy.
262
+ If unknown, use list_memory_projects() to discover available projects.
259
263
  page: The page number of results to return (default 1)
260
264
  page_size: The number of results to return per page (default 10)
261
265
  search_type: Type of search to perform, one of: "text", "title", "permalink" (default: "text")
262
266
  types: Optional list of note types to search (e.g., ["note", "person"])
263
267
  entity_types: Optional list of entity types to filter by (e.g., ["entity", "observation"])
264
268
  after_date: Optional date filter for recent content (e.g., "1 week", "2d", "2024-01-01")
265
- project: Optional project name to search in. If not provided, uses current active project.
269
+ context: Optional FastMCP context for performance caching.
266
270
 
267
271
  Returns:
268
272
  SearchResponse with results and pagination info, or helpful error guidance if search fails
@@ -288,37 +292,43 @@ async def search_notes(
288
292
 
289
293
  # Search with type filter
290
294
  results = await search_notes(
291
- query="meeting notes",
295
+ "meeting notes",
292
296
  types=["entity"],
293
297
  )
294
298
 
295
299
  # Search with entity type filter
296
300
  results = await search_notes(
297
- query="meeting notes",
301
+ "meeting notes",
298
302
  entity_types=["observation"],
299
303
  )
300
304
 
301
305
  # Search for recent content
302
306
  results = await search_notes(
303
- query="bug report",
307
+ "bug report",
304
308
  after_date="1 week"
305
309
  )
306
310
 
307
311
  # Pattern matching on permalinks
308
312
  results = await search_notes(
309
- query="docs/meeting-*",
313
+ "docs/meeting-*",
310
314
  search_type="permalink"
311
315
  )
312
316
 
313
- # Search in specific project
314
- results = await search_notes("meeting notes", project="work-project")
317
+ # Title-only search
318
+ results = await search_notes(
319
+ "Machine Learning",
320
+ search_type="title"
321
+ )
315
322
 
316
323
  # Complex search with multiple filters
317
324
  results = await search_notes(
318
- query="(bug OR issue) AND NOT resolved",
325
+ "(bug OR issue) AND NOT resolved",
319
326
  types=["entity"],
320
327
  after_date="2024-01-01"
321
328
  )
329
+
330
+ # Explicit project specification
331
+ results = await search_notes("project planning", project="my-project")
322
332
  """
323
333
  # Create a SearchQuery object based on the parameters
324
334
  search_query = SearchQuery()
@@ -343,29 +353,32 @@ async def search_notes(
343
353
  if after_date:
344
354
  search_query.after_date = after_date
345
355
 
346
- active_project = get_active_project(project)
347
- project_url = active_project.project_url
348
-
349
- logger.info(f"Searching for {search_query}")
350
-
351
- try:
352
- response = await call_post(
353
- client,
354
- f"{project_url}/search/",
355
- json=search_query.model_dump(),
356
- params={"page": page, "page_size": page_size},
357
- )
358
- result = SearchResponse.model_validate(response.json())
359
-
360
- # Check if we got no results and provide helpful guidance
361
- if not result.results:
362
- logger.info(f"Search returned no results for query: {query}")
363
- # Don't treat this as an error, but the user might want guidance
364
- # We return the empty result as normal - the user can decide if they need help
365
-
366
- return result
367
-
368
- except Exception as e:
369
- logger.error(f"Search failed for query '{query}': {e}")
370
- # Return formatted error message as string for better user experience
371
- return _format_search_error_response(str(e), query, search_type)
356
+ async with get_client() as client:
357
+ active_project = await get_active_project(client, project, context)
358
+ project_url = active_project.project_url
359
+
360
+ logger.info(f"Searching for {search_query} in project {active_project.name}")
361
+
362
+ try:
363
+ response = await call_post(
364
+ client,
365
+ f"{project_url}/search/",
366
+ json=search_query.model_dump(),
367
+ params={"page": page, "page_size": page_size},
368
+ )
369
+ result = SearchResponse.model_validate(response.json())
370
+
371
+ # Check if we got no results and provide helpful guidance
372
+ if not result.results:
373
+ logger.info(
374
+ f"Search returned no results for query: {query} in project {active_project.name}"
375
+ )
376
+ # Don't treat this as an error, but the user might want guidance
377
+ # We return the empty result as normal - the user can decide if they need help
378
+
379
+ return result
380
+
381
+ except Exception as e:
382
+ logger.error(f"Search failed for query '{query}': {e}, project: {active_project.name}")
383
+ # Return formatted error message as string for better user experience
384
+ return _format_search_error_response(active_project.name, str(e), query, search_type)