basic-memory 0.14.4__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 (82) 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/knowledge_router.py +25 -8
  5. basic_memory/api/routers/project_router.py +99 -4
  6. basic_memory/cli/app.py +9 -28
  7. basic_memory/cli/auth.py +277 -0
  8. basic_memory/cli/commands/cloud/__init__.py +5 -0
  9. basic_memory/cli/commands/cloud/api_client.py +112 -0
  10. basic_memory/cli/commands/cloud/bisync_commands.py +818 -0
  11. basic_memory/cli/commands/cloud/core_commands.py +288 -0
  12. basic_memory/cli/commands/cloud/mount_commands.py +295 -0
  13. basic_memory/cli/commands/cloud/rclone_config.py +288 -0
  14. basic_memory/cli/commands/cloud/rclone_installer.py +198 -0
  15. basic_memory/cli/commands/command_utils.py +60 -0
  16. basic_memory/cli/commands/import_memory_json.py +0 -4
  17. basic_memory/cli/commands/mcp.py +16 -4
  18. basic_memory/cli/commands/project.py +139 -142
  19. basic_memory/cli/commands/status.py +34 -22
  20. basic_memory/cli/commands/sync.py +45 -228
  21. basic_memory/cli/commands/tool.py +87 -16
  22. basic_memory/cli/main.py +1 -0
  23. basic_memory/config.py +76 -12
  24. basic_memory/db.py +104 -3
  25. basic_memory/deps.py +20 -3
  26. basic_memory/file_utils.py +37 -13
  27. basic_memory/ignore_utils.py +295 -0
  28. basic_memory/markdown/plugins.py +9 -7
  29. basic_memory/mcp/async_client.py +22 -10
  30. basic_memory/mcp/project_context.py +141 -0
  31. basic_memory/mcp/prompts/ai_assistant_guide.py +49 -4
  32. basic_memory/mcp/prompts/continue_conversation.py +1 -1
  33. basic_memory/mcp/prompts/recent_activity.py +116 -32
  34. basic_memory/mcp/prompts/search.py +1 -1
  35. basic_memory/mcp/prompts/utils.py +11 -4
  36. basic_memory/mcp/resources/ai_assistant_guide.md +179 -41
  37. basic_memory/mcp/resources/project_info.py +20 -6
  38. basic_memory/mcp/server.py +0 -37
  39. basic_memory/mcp/tools/__init__.py +5 -6
  40. basic_memory/mcp/tools/build_context.py +29 -19
  41. basic_memory/mcp/tools/canvas.py +19 -8
  42. basic_memory/mcp/tools/chatgpt_tools.py +178 -0
  43. basic_memory/mcp/tools/delete_note.py +67 -34
  44. basic_memory/mcp/tools/edit_note.py +55 -39
  45. basic_memory/mcp/tools/headers.py +44 -0
  46. basic_memory/mcp/tools/list_directory.py +18 -8
  47. basic_memory/mcp/tools/move_note.py +119 -41
  48. basic_memory/mcp/tools/project_management.py +61 -228
  49. basic_memory/mcp/tools/read_content.py +28 -12
  50. basic_memory/mcp/tools/read_note.py +83 -46
  51. basic_memory/mcp/tools/recent_activity.py +441 -42
  52. basic_memory/mcp/tools/search.py +82 -70
  53. basic_memory/mcp/tools/sync_status.py +5 -4
  54. basic_memory/mcp/tools/utils.py +19 -0
  55. basic_memory/mcp/tools/view_note.py +31 -6
  56. basic_memory/mcp/tools/write_note.py +65 -14
  57. basic_memory/models/knowledge.py +12 -6
  58. basic_memory/models/project.py +6 -2
  59. basic_memory/repository/entity_repository.py +29 -82
  60. basic_memory/repository/relation_repository.py +13 -0
  61. basic_memory/repository/repository.py +2 -2
  62. basic_memory/repository/search_repository.py +4 -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/entity_service.py +75 -45
  71. basic_memory/services/initialization.py +30 -11
  72. basic_memory/services/project_service.py +13 -23
  73. basic_memory/sync/sync_service.py +145 -21
  74. basic_memory/sync/watch_service.py +101 -40
  75. basic_memory/utils.py +14 -4
  76. {basic_memory-0.14.4.dist-info → basic_memory-0.15.0.dist-info}/METADATA +7 -6
  77. basic_memory-0.15.0.dist-info/RECORD +147 -0
  78. basic_memory/mcp/project_session.py +0 -120
  79. basic_memory-0.14.4.dist-info/RECORD +0 -133
  80. {basic_memory-0.14.4.dist-info → basic_memory-0.15.0.dist-info}/WHEEL +0 -0
  81. {basic_memory-0.14.4.dist-info → basic_memory-0.15.0.dist-info}/entry_points.txt +0 -0
  82. {basic_memory-0.14.4.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,50 +19,76 @@ 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)
58
83
 
59
84
  # Validate identifier to prevent path traversal attacks
60
85
  # We need to check both the raw identifier and the processed path
61
86
  processed_path = memory_url_path(identifier)
62
87
  project_path = active_project.home
63
-
64
- if not validate_project_path(identifier, project_path) or not validate_project_path(processed_path, project_path):
88
+
89
+ if not validate_project_path(identifier, project_path) or not validate_project_path(
90
+ processed_path, project_path
91
+ ):
65
92
  logger.warning(
66
93
  "Attempted path traversal attack blocked",
67
94
  identifier=identifier,
@@ -83,7 +110,7 @@ async def read_note(
83
110
  # Get the file via REST API - first try direct permalink lookup
84
111
  entity_path = memory_url_path(identifier)
85
112
  path = f"{project_url}/resource/{entity_path}"
86
- 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}")
87
114
 
88
115
  try:
89
116
  # Try direct lookup first
@@ -99,9 +126,12 @@ async def read_note(
99
126
 
100
127
  # Fallback 1: Try title search via API
101
128
  logger.info(f"Search title for: {identifier}")
102
- 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
+ )
103
132
 
104
- 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:
105
135
  result = title_results.results[0] # Get the first/best match
106
136
  if result.permalink:
107
137
  try:
@@ -119,58 +149,64 @@ async def read_note(
119
149
  f"Failed to fetch content for found title match {result.permalink}: {e}"
120
150
  )
121
151
  else:
122
- 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
+ )
123
155
 
124
156
  # Fallback 2: Text search as a last resort
125
157
  logger.info(f"Title search failed, trying text search for: {identifier}")
126
- 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
+ )
127
161
 
128
162
  # We didn't find a direct match, construct a helpful error message
129
- 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:
130
165
  # No results at all
131
- return format_not_found_message(identifier)
166
+ return format_not_found_message(active_project.name, identifier)
132
167
  else:
133
168
  # We found some related results
134
- return format_related_results(identifier, text_results.results[:5])
169
+ return format_related_results(active_project.name, identifier, text_results.results[:5])
135
170
 
136
171
 
137
- def format_not_found_message(identifier: str) -> str:
172
+ def format_not_found_message(project: str | None, identifier: str) -> str:
138
173
  """Format a helpful message when no note was found."""
139
174
  return dedent(f"""
140
- # Note Not Found: "{identifier}"
141
-
175
+ # Note Not Found in {project}: "{identifier}"
176
+
142
177
  I couldn't find any notes matching "{identifier}". Here are some suggestions:
143
-
178
+
144
179
  ## Check Identifier Type
145
180
  - If you provided a title, try using the exact permalink instead
146
181
  - If you provided a permalink, check for typos or try a broader search
147
-
182
+
148
183
  ## Search Instead
149
184
  Try searching for related content:
150
185
  ```
151
- search_notes(query="{identifier}")
186
+ search_notes(project="{project}", query="{identifier}")
152
187
  ```
153
-
188
+
154
189
  ## Recent Activity
155
190
  Check recently modified notes:
156
191
  ```
157
192
  recent_activity(timeframe="7d")
158
193
  ```
159
-
194
+
160
195
  ## Create New Note
161
196
  This might be a good opportunity to create a new note on this topic:
162
197
  ```
163
198
  write_note(
199
+ project="{project}",
164
200
  title="{identifier.capitalize()}",
165
201
  content='''
166
202
  # {identifier.capitalize()}
167
-
203
+
168
204
  ## Overview
169
205
  [Your content here]
170
-
206
+
171
207
  ## Observations
172
208
  - [category] [Observation about {identifier}]
173
-
209
+
174
210
  ## Relations
175
211
  - relates_to [[Related Topic]]
176
212
  ''',
@@ -180,13 +216,13 @@ def format_not_found_message(identifier: str) -> str:
180
216
  """)
181
217
 
182
218
 
183
- def format_related_results(identifier: str, results) -> str:
219
+ def format_related_results(project: str | None, identifier: str, results) -> str:
184
220
  """Format a helpful message with related results when an exact match wasn't found."""
185
221
  message = dedent(f"""
186
- # Note Not Found: "{identifier}"
187
-
222
+ # Note Not Found in {project}: "{identifier}"
223
+
188
224
  I couldn't find an exact match for "{identifier}", but I found some related notes:
189
-
225
+
190
226
  """)
191
227
 
192
228
  for i, result in enumerate(results):
@@ -194,28 +230,29 @@ def format_related_results(identifier: str, results) -> str:
194
230
  ## {i + 1}. {result.title}
195
231
  - **Type**: {result.type.value}
196
232
  - **Permalink**: {result.permalink}
197
-
233
+
198
234
  You can read this note with:
199
235
  ```
200
- read_note("{result.permalink}")
236
+ read_note(project="{project}", {result.permalink}")
201
237
  ```
202
-
238
+
203
239
  """)
204
240
 
205
- message += dedent("""
241
+ message += dedent(f"""
206
242
  ## Try More Specific Lookup
207
243
  For exact matches, try using the full permalink from one of the results above.
208
-
244
+
209
245
  ## Search For More Results
210
246
  To see more related content:
211
247
  ```
212
- search_notes(query="{identifier}")
248
+ search_notes(project="{project}", query="{identifier}")
213
249
  ```
214
-
250
+
215
251
  ## Create New Note
216
252
  If none of these match what you're looking for, consider creating a new note:
217
253
  ```
218
254
  write_note(
255
+ project="{project}",
219
256
  title="[Your title]",
220
257
  content="[Your content]",
221
258
  folder="notes"