basic-memory 0.14.3__py3-none-any.whl → 0.15.0__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 (90) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  3. basic_memory/api/app.py +10 -4
  4. basic_memory/api/routers/knowledge_router.py +25 -8
  5. basic_memory/api/routers/project_router.py +99 -4
  6. basic_memory/api/routers/resource_router.py +3 -3
  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 +60 -0
  17. basic_memory/cli/commands/import_memory_json.py +0 -4
  18. basic_memory/cli/commands/mcp.py +16 -4
  19. basic_memory/cli/commands/project.py +141 -145
  20. basic_memory/cli/commands/status.py +34 -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 +96 -20
  25. basic_memory/db.py +104 -3
  26. basic_memory/deps.py +20 -3
  27. basic_memory/file_utils.py +89 -0
  28. basic_memory/ignore_utils.py +295 -0
  29. basic_memory/importers/chatgpt_importer.py +1 -1
  30. basic_memory/importers/utils.py +2 -2
  31. basic_memory/markdown/entity_parser.py +2 -2
  32. basic_memory/markdown/markdown_processor.py +2 -2
  33. basic_memory/markdown/plugins.py +39 -21
  34. basic_memory/markdown/utils.py +1 -1
  35. basic_memory/mcp/async_client.py +22 -10
  36. basic_memory/mcp/project_context.py +141 -0
  37. basic_memory/mcp/prompts/ai_assistant_guide.py +49 -4
  38. basic_memory/mcp/prompts/continue_conversation.py +1 -1
  39. basic_memory/mcp/prompts/recent_activity.py +116 -32
  40. basic_memory/mcp/prompts/search.py +1 -1
  41. basic_memory/mcp/prompts/utils.py +11 -4
  42. basic_memory/mcp/resources/ai_assistant_guide.md +179 -41
  43. basic_memory/mcp/resources/project_info.py +20 -6
  44. basic_memory/mcp/server.py +0 -37
  45. basic_memory/mcp/tools/__init__.py +5 -6
  46. basic_memory/mcp/tools/build_context.py +39 -19
  47. basic_memory/mcp/tools/canvas.py +19 -8
  48. basic_memory/mcp/tools/chatgpt_tools.py +178 -0
  49. basic_memory/mcp/tools/delete_note.py +67 -34
  50. basic_memory/mcp/tools/edit_note.py +55 -39
  51. basic_memory/mcp/tools/headers.py +44 -0
  52. basic_memory/mcp/tools/list_directory.py +18 -8
  53. basic_memory/mcp/tools/move_note.py +119 -41
  54. basic_memory/mcp/tools/project_management.py +77 -229
  55. basic_memory/mcp/tools/read_content.py +28 -12
  56. basic_memory/mcp/tools/read_note.py +97 -57
  57. basic_memory/mcp/tools/recent_activity.py +441 -42
  58. basic_memory/mcp/tools/search.py +82 -70
  59. basic_memory/mcp/tools/sync_status.py +5 -4
  60. basic_memory/mcp/tools/utils.py +19 -0
  61. basic_memory/mcp/tools/view_note.py +31 -6
  62. basic_memory/mcp/tools/write_note.py +65 -14
  63. basic_memory/models/knowledge.py +19 -2
  64. basic_memory/models/project.py +6 -2
  65. basic_memory/repository/entity_repository.py +31 -84
  66. basic_memory/repository/project_repository.py +1 -1
  67. basic_memory/repository/relation_repository.py +13 -0
  68. basic_memory/repository/repository.py +2 -2
  69. basic_memory/repository/search_repository.py +9 -3
  70. basic_memory/schemas/__init__.py +6 -0
  71. basic_memory/schemas/base.py +70 -12
  72. basic_memory/schemas/cloud.py +46 -0
  73. basic_memory/schemas/memory.py +99 -18
  74. basic_memory/schemas/project_info.py +9 -10
  75. basic_memory/schemas/sync_report.py +48 -0
  76. basic_memory/services/context_service.py +35 -11
  77. basic_memory/services/directory_service.py +7 -0
  78. basic_memory/services/entity_service.py +82 -52
  79. basic_memory/services/initialization.py +30 -11
  80. basic_memory/services/project_service.py +23 -33
  81. basic_memory/sync/sync_service.py +148 -24
  82. basic_memory/sync/watch_service.py +128 -44
  83. basic_memory/utils.py +181 -109
  84. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/METADATA +26 -96
  85. basic_memory-0.15.0.dist-info/RECORD +147 -0
  86. basic_memory/mcp/project_session.py +0 -120
  87. basic_memory-0.14.3.dist-info/RECORD +0 -132
  88. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/WHEEL +0 -0
  89. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/entry_points.txt +0 -0
  90. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/licenses/LICENSE +0 -0
@@ -4,12 +4,13 @@ from textwrap import dedent
4
4
  from typing import Optional
5
5
 
6
6
  from loguru import logger
7
+ from fastmcp import Context
7
8
 
8
9
  from basic_memory.mcp.async_client import 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.search import search_notes
11
13
  from basic_memory.mcp.tools.utils import call_get
12
- from basic_memory.mcp.project_session import get_active_project
13
14
  from basic_memory.schemas.memory import memory_url_path
14
15
  from basic_memory.utils import validate_project_path
15
16
 
@@ -18,43 +19,83 @@ from basic_memory.utils import validate_project_path
18
19
  description="Read a markdown note by title or permalink.",
19
20
  )
20
21
  async def read_note(
21
- identifier: str, page: int = 1, page_size: int = 10, project: Optional[str] = None
22
+ identifier: str,
23
+ project: Optional[str] = None,
24
+ page: int = 1,
25
+ page_size: int = 10,
26
+ context: Context | None = None,
22
27
  ) -> str:
23
- """Read a markdown note from the knowledge base.
28
+ """Return the raw markdown for a note, or guidance text if no match is found.
24
29
 
25
- This tool finds and retrieves a note by its title, permalink, or content search,
30
+ Finds and retrieves a note by its title, permalink, or content search,
26
31
  returning the raw markdown content including observations, relations, and metadata.
27
- It will try multiple lookup strategies to find the most relevant note.
32
+
33
+ Project Resolution:
34
+ Server resolves projects in this order: Single Project Mode → project parameter → default project.
35
+ If project unknown, use list_memory_projects() or recent_activity() first.
36
+
37
+ This tool will try multiple lookup strategies to find the most relevant note:
38
+ 1. Direct permalink lookup
39
+ 2. Title search fallback
40
+ 3. Text search as last resort
28
41
 
29
42
  Args:
43
+ project: Project name to read from. Optional - server will resolve using the
44
+ hierarchy above. If unknown, use list_memory_projects() to discover
45
+ available projects.
30
46
  identifier: The title or permalink of the note to read
31
47
  Can be a full memory:// URL, a permalink, a title, or search text
32
48
  page: Page number for paginated results (default: 1)
33
49
  page_size: Number of items per page (default: 10)
34
- project: Optional project name to read from. If not provided, uses current active project.
50
+ context: Optional FastMCP context for performance caching.
35
51
 
36
52
  Returns:
37
53
  The full markdown content of the note if found, or helpful guidance if not found.
54
+ Content includes frontmatter, observations, relations, and all markdown formatting.
38
55
 
39
56
  Examples:
40
57
  # Read by permalink
41
- read_note("specs/search-spec")
58
+ read_note("my-research", "specs/search-spec")
42
59
 
43
60
  # Read by title
44
- read_note("Search Specification")
61
+ read_note("work-project", "Search Specification")
45
62
 
46
63
  # Read with memory URL
47
- read_note("memory://specs/search-spec")
64
+ read_note("my-research", "memory://specs/search-spec")
48
65
 
49
66
  # Read with pagination
50
- read_note("Project Updates", page=2, page_size=5)
67
+ read_note("work-project", "Project Updates", page=2, page_size=5)
68
+
69
+ # Read recent meeting notes
70
+ read_note("team-docs", "Weekly Standup")
51
71
 
52
- # Read from specific project
53
- read_note("Meeting Notes", project="work-project")
72
+ Raises:
73
+ HTTPError: If project doesn't exist or is inaccessible
74
+ SecurityError: If identifier attempts path traversal
75
+
76
+ Note:
77
+ If the exact note isn't found, this tool provides helpful suggestions
78
+ including related notes, search commands, and note creation templates.
54
79
  """
55
80
 
56
- # Get the active project first to check project-specific sync status
57
- active_project = get_active_project(project)
81
+ # Get and validate the project
82
+ active_project = await get_active_project(client, project, context)
83
+
84
+ # Validate identifier to prevent path traversal attacks
85
+ # We need to check both the raw identifier and the processed path
86
+ processed_path = memory_url_path(identifier)
87
+ project_path = active_project.home
88
+
89
+ if not validate_project_path(identifier, project_path) or not validate_project_path(
90
+ processed_path, project_path
91
+ ):
92
+ logger.warning(
93
+ "Attempted path traversal attack blocked",
94
+ identifier=identifier,
95
+ processed_path=processed_path,
96
+ project=active_project.name,
97
+ )
98
+ return f"# Error\n\nIdentifier '{identifier}' is not allowed - paths must stay within project boundaries"
58
99
 
59
100
  # Check migration status and wait briefly if needed
60
101
  from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
@@ -68,19 +109,8 @@ async def read_note(
68
109
 
69
110
  # Get the file via REST API - first try direct permalink lookup
70
111
  entity_path = memory_url_path(identifier)
71
-
72
- # Validate path to prevent path traversal attacks
73
- project_path = active_project.home
74
- if not validate_project_path(entity_path, project_path):
75
- logger.warning(
76
- "Attempted path traversal attack blocked",
77
- identifier=identifier,
78
- entity_path=entity_path,
79
- project=active_project.name,
80
- )
81
- return f"# Error\n\nPath '{identifier}' is not allowed - paths must stay within project boundaries"
82
112
  path = f"{project_url}/resource/{entity_path}"
83
- logger.info(f"Attempting to read note from URL: {path}")
113
+ logger.info(f"Attempting to read note from Project: {active_project.name} URL: {path}")
84
114
 
85
115
  try:
86
116
  # Try direct lookup first
@@ -96,9 +126,12 @@ async def read_note(
96
126
 
97
127
  # Fallback 1: Try title search via API
98
128
  logger.info(f"Search title for: {identifier}")
99
- title_results = await search_notes.fn(query=identifier, search_type="title", project=project)
129
+ title_results = await search_notes.fn(
130
+ query=identifier, search_type="title", project=project, context=context
131
+ )
100
132
 
101
- if title_results and title_results.results:
133
+ # Handle both SearchResponse object and error strings
134
+ if title_results and hasattr(title_results, "results") and title_results.results:
102
135
  result = title_results.results[0] # Get the first/best match
103
136
  if result.permalink:
104
137
  try:
@@ -116,58 +149,64 @@ async def read_note(
116
149
  f"Failed to fetch content for found title match {result.permalink}: {e}"
117
150
  )
118
151
  else:
119
- logger.info(f"No results in title search for: {identifier}")
152
+ logger.info(
153
+ f"No results in title search for: {identifier} in project {active_project.name}"
154
+ )
120
155
 
121
156
  # Fallback 2: Text search as a last resort
122
157
  logger.info(f"Title search failed, trying text search for: {identifier}")
123
- text_results = await search_notes.fn(query=identifier, search_type="text", project=project)
158
+ text_results = await search_notes.fn(
159
+ query=identifier, search_type="text", project=project, context=context
160
+ )
124
161
 
125
162
  # We didn't find a direct match, construct a helpful error message
126
- if not text_results or not text_results.results:
163
+ # Handle both SearchResponse object and error strings
164
+ if not text_results or not hasattr(text_results, "results") or not text_results.results:
127
165
  # No results at all
128
- return format_not_found_message(identifier)
166
+ return format_not_found_message(active_project.name, identifier)
129
167
  else:
130
168
  # We found some related results
131
- return format_related_results(identifier, text_results.results[:5])
169
+ return format_related_results(active_project.name, identifier, text_results.results[:5])
132
170
 
133
171
 
134
- def format_not_found_message(identifier: str) -> str:
172
+ def format_not_found_message(project: str | None, identifier: str) -> str:
135
173
  """Format a helpful message when no note was found."""
136
174
  return dedent(f"""
137
- # Note Not Found: "{identifier}"
138
-
139
- I searched for "{identifier}" using multiple methods (direct lookup, title search, and text search) but couldn't find any matching notes. Here are some suggestions:
140
-
175
+ # Note Not Found in {project}: "{identifier}"
176
+
177
+ I couldn't find any notes matching "{identifier}". Here are some suggestions:
178
+
141
179
  ## Check Identifier Type
142
180
  - If you provided a title, try using the exact permalink instead
143
181
  - If you provided a permalink, check for typos or try a broader search
144
-
182
+
145
183
  ## Search Instead
146
184
  Try searching for related content:
147
185
  ```
148
- search_notes(query="{identifier}")
186
+ search_notes(project="{project}", query="{identifier}")
149
187
  ```
150
-
188
+
151
189
  ## Recent Activity
152
190
  Check recently modified notes:
153
191
  ```
154
192
  recent_activity(timeframe="7d")
155
193
  ```
156
-
194
+
157
195
  ## Create New Note
158
196
  This might be a good opportunity to create a new note on this topic:
159
197
  ```
160
198
  write_note(
199
+ project="{project}",
161
200
  title="{identifier.capitalize()}",
162
201
  content='''
163
202
  # {identifier.capitalize()}
164
-
203
+
165
204
  ## Overview
166
205
  [Your content here]
167
-
206
+
168
207
  ## Observations
169
208
  - [category] [Observation about {identifier}]
170
-
209
+
171
210
  ## Relations
172
211
  - relates_to [[Related Topic]]
173
212
  ''',
@@ -177,13 +216,13 @@ def format_not_found_message(identifier: str) -> str:
177
216
  """)
178
217
 
179
218
 
180
- def format_related_results(identifier: str, results) -> str:
219
+ def format_related_results(project: str | None, identifier: str, results) -> str:
181
220
  """Format a helpful message with related results when an exact match wasn't found."""
182
221
  message = dedent(f"""
183
- # Note Not Found: "{identifier}"
184
-
185
- I searched for "{identifier}" using direct lookup and title search but couldn't find an exact match. However, I found some related notes through text search:
186
-
222
+ # Note Not Found in {project}: "{identifier}"
223
+
224
+ I couldn't find an exact match for "{identifier}", but I found some related notes:
225
+
187
226
  """)
188
227
 
189
228
  for i, result in enumerate(results):
@@ -191,28 +230,29 @@ def format_related_results(identifier: str, results) -> str:
191
230
  ## {i + 1}. {result.title}
192
231
  - **Type**: {result.type.value}
193
232
  - **Permalink**: {result.permalink}
194
-
233
+
195
234
  You can read this note with:
196
235
  ```
197
- read_note("{result.permalink}")
236
+ read_note(project="{project}", {result.permalink}")
198
237
  ```
199
-
238
+
200
239
  """)
201
240
 
202
- message += dedent("""
241
+ message += dedent(f"""
203
242
  ## Try More Specific Lookup
204
243
  For exact matches, try using the full permalink from one of the results above.
205
-
244
+
206
245
  ## Search For More Results
207
246
  To see more related content:
208
247
  ```
209
- search_notes(query="{identifier}")
248
+ search_notes(project="{project}", query="{identifier}")
210
249
  ```
211
-
250
+
212
251
  ## Create New Note
213
252
  If none of these match what you're looking for, consider creating a new note:
214
253
  ```
215
254
  write_note(
255
+ project="{project}",
216
256
  title="[Your title]",
217
257
  content="[Your content]",
218
258
  folder="notes"