basic-memory 0.15.0__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 (47) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/api/routers/directory_router.py +23 -2
  3. basic_memory/api/routers/project_router.py +1 -0
  4. basic_memory/cli/auth.py +2 -2
  5. basic_memory/cli/commands/command_utils.py +11 -28
  6. basic_memory/cli/commands/mcp.py +72 -67
  7. basic_memory/cli/commands/project.py +54 -49
  8. basic_memory/cli/commands/status.py +6 -15
  9. basic_memory/config.py +55 -9
  10. basic_memory/deps.py +7 -5
  11. basic_memory/ignore_utils.py +7 -7
  12. basic_memory/mcp/async_client.py +102 -4
  13. basic_memory/mcp/prompts/continue_conversation.py +16 -15
  14. basic_memory/mcp/prompts/search.py +12 -11
  15. basic_memory/mcp/resources/ai_assistant_guide.md +185 -453
  16. basic_memory/mcp/resources/project_info.py +9 -7
  17. basic_memory/mcp/tools/build_context.py +40 -39
  18. basic_memory/mcp/tools/canvas.py +21 -20
  19. basic_memory/mcp/tools/chatgpt_tools.py +11 -2
  20. basic_memory/mcp/tools/delete_note.py +22 -21
  21. basic_memory/mcp/tools/edit_note.py +105 -104
  22. basic_memory/mcp/tools/list_directory.py +98 -95
  23. basic_memory/mcp/tools/move_note.py +127 -125
  24. basic_memory/mcp/tools/project_management.py +101 -98
  25. basic_memory/mcp/tools/read_content.py +64 -63
  26. basic_memory/mcp/tools/read_note.py +88 -88
  27. basic_memory/mcp/tools/recent_activity.py +139 -135
  28. basic_memory/mcp/tools/search.py +27 -26
  29. basic_memory/mcp/tools/sync_status.py +133 -128
  30. basic_memory/mcp/tools/utils.py +0 -15
  31. basic_memory/mcp/tools/view_note.py +14 -28
  32. basic_memory/mcp/tools/write_note.py +97 -87
  33. basic_memory/repository/entity_repository.py +60 -0
  34. basic_memory/repository/repository.py +16 -3
  35. basic_memory/repository/search_repository.py +42 -0
  36. basic_memory/schemas/project_info.py +1 -1
  37. basic_memory/services/directory_service.py +124 -3
  38. basic_memory/services/entity_service.py +31 -9
  39. basic_memory/services/project_service.py +97 -10
  40. basic_memory/services/search_service.py +16 -8
  41. basic_memory/sync/sync_service.py +28 -13
  42. {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/METADATA +51 -4
  43. {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/RECORD +46 -47
  44. basic_memory/mcp/tools/headers.py +0 -44
  45. {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/WHEEL +0 -0
  46. {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/entry_points.txt +0 -0
  47. {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -5,7 +5,7 @@ from typing import Optional
5
5
  from loguru import logger
6
6
  from fastmcp import Context
7
7
 
8
- from basic_memory.mcp.async_client import client
8
+ from basic_memory.mcp.async_client import get_client
9
9
  from basic_memory.mcp.project_context import get_active_project
10
10
  from basic_memory.mcp.server import mcp
11
11
  from basic_memory.mcp.tools.utils import call_get
@@ -59,11 +59,13 @@ async def project_info(
59
59
  print(f"Basic Memory version: {info.system.version}")
60
60
  """
61
61
  logger.info("Getting project info")
62
- project_config = await get_active_project(client, project, context)
63
- project_url = project_config.permalink
64
62
 
65
- # Call the API endpoint
66
- response = await call_get(client, f"{project_url}/project/info")
63
+ async with get_client() as client:
64
+ project_config = await get_active_project(client, project, context)
65
+ project_url = project_config.permalink
67
66
 
68
- # Convert response to ProjectInfoResponse
69
- return ProjectInfoResponse.model_validate(response.json())
67
+ # Call the API endpoint
68
+ response = await call_get(client, f"{project_url}/project/info")
69
+
70
+ # Convert response to ProjectInfoResponse
71
+ return ProjectInfoResponse.model_validate(response.json())
@@ -5,7 +5,7 @@ from typing import Optional
5
5
  from loguru import logger
6
6
  from fastmcp import Context
7
7
 
8
- from basic_memory.mcp.async_client import client
8
+ from basic_memory.mcp.async_client import get_client
9
9
  from basic_memory.mcp.project_context import get_active_project
10
10
  from basic_memory.mcp.server import mcp
11
11
  from basic_memory.mcp.tools.utils import call_get
@@ -102,42 +102,43 @@ async def build_context(
102
102
 
103
103
  # URL is already validated and normalized by MemoryUrl type annotation
104
104
 
105
- # Get the active project using the new stateless approach
106
- active_project = await get_active_project(client, project, context)
107
-
108
- # Check migration status and wait briefly if needed
109
- from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
110
-
111
- migration_status = await wait_for_migration_or_return_status(
112
- timeout=5.0, project_name=active_project.name
113
- )
114
- if migration_status: # pragma: no cover
115
- # Return a proper GraphContext with status message
116
- from basic_memory.schemas.memory import MemoryMetadata
117
- from datetime import datetime
118
-
119
- return GraphContext(
120
- results=[],
121
- metadata=MemoryMetadata(
122
- depth=depth or 1,
123
- timeframe=timeframe,
124
- generated_at=datetime.now().astimezone(),
125
- primary_count=0,
126
- related_count=0,
127
- uri=migration_status, # Include status in metadata
128
- ),
105
+ async with get_client() as client:
106
+ # Get the active project using the new stateless approach
107
+ active_project = await get_active_project(client, project, context)
108
+
109
+ # Check migration status and wait briefly if needed
110
+ from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
111
+
112
+ migration_status = await wait_for_migration_or_return_status(
113
+ timeout=5.0, project_name=active_project.name
114
+ )
115
+ if migration_status: # pragma: no cover
116
+ # Return a proper GraphContext with status message
117
+ from basic_memory.schemas.memory import MemoryMetadata
118
+ from datetime import datetime
119
+
120
+ return GraphContext(
121
+ results=[],
122
+ metadata=MemoryMetadata(
123
+ depth=depth or 1,
124
+ timeframe=timeframe,
125
+ generated_at=datetime.now().astimezone(),
126
+ primary_count=0,
127
+ related_count=0,
128
+ uri=migration_status, # Include status in metadata
129
+ ),
130
+ )
131
+ project_url = active_project.project_url
132
+
133
+ response = await call_get(
134
+ client,
135
+ f"{project_url}/memory/{memory_url_path(url)}",
136
+ params={
137
+ "depth": depth,
138
+ "timeframe": timeframe,
139
+ "page": page,
140
+ "page_size": page_size,
141
+ "max_related": max_related,
142
+ },
129
143
  )
130
- project_url = active_project.project_url
131
-
132
- response = await call_get(
133
- client,
134
- f"{project_url}/memory/{memory_url_path(url)}",
135
- params={
136
- "depth": depth,
137
- "timeframe": timeframe,
138
- "page": page,
139
- "page_size": page_size,
140
- "max_related": max_related,
141
- },
142
- )
143
- return GraphContext.model_validate(response.json())
144
+ return GraphContext.model_validate(response.json())
@@ -9,7 +9,7 @@ from typing import Dict, List, Any, Optional
9
9
  from loguru import logger
10
10
  from fastmcp import Context
11
11
 
12
- from basic_memory.mcp.async_client import client
12
+ from basic_memory.mcp.async_client import get_client
13
13
  from basic_memory.mcp.project_context import get_active_project
14
14
  from basic_memory.mcp.server import mcp
15
15
  from basic_memory.mcp.tools.utils import call_put
@@ -94,29 +94,30 @@ async def canvas(
94
94
  Raises:
95
95
  ToolError: If project doesn't exist or folder path is invalid
96
96
  """
97
- active_project = await get_active_project(client, project, context)
98
- project_url = active_project.project_url
97
+ async with get_client() as client:
98
+ active_project = await get_active_project(client, project, context)
99
+ project_url = active_project.project_url
99
100
 
100
- # Ensure path has .canvas extension
101
- file_title = title if title.endswith(".canvas") else f"{title}.canvas"
102
- file_path = f"{folder}/{file_title}"
101
+ # Ensure path has .canvas extension
102
+ file_title = title if title.endswith(".canvas") else f"{title}.canvas"
103
+ file_path = f"{folder}/{file_title}"
103
104
 
104
- # Create canvas data structure
105
- canvas_data = {"nodes": nodes, "edges": edges}
105
+ # Create canvas data structure
106
+ canvas_data = {"nodes": nodes, "edges": edges}
106
107
 
107
- # Convert to JSON
108
- canvas_json = json.dumps(canvas_data, indent=2)
108
+ # Convert to JSON
109
+ canvas_json = json.dumps(canvas_data, indent=2)
109
110
 
110
- # Write the file using the resource API
111
- logger.info(f"Creating canvas file: {file_path} in project {project}")
112
- response = await call_put(client, f"{project_url}/resource/{file_path}", json=canvas_json)
111
+ # Write the file using the resource API
112
+ logger.info(f"Creating canvas file: {file_path} in project {project}")
113
+ response = await call_put(client, f"{project_url}/resource/{file_path}", json=canvas_json)
113
114
 
114
- # Parse response
115
- result = response.json()
116
- logger.debug(result)
115
+ # Parse response
116
+ result = response.json()
117
+ logger.debug(result)
117
118
 
118
- # Build summary
119
- action = "Created" if response.status_code == 201 else "Updated"
120
- summary = [f"# {action}: {file_path}", "\nThe canvas is ready to open in Obsidian."]
119
+ # Build summary
120
+ action = "Created" if response.status_code == 201 else "Updated"
121
+ summary = [f"# {action}: {file_path}", "\nThe canvas is ready to open in Obsidian."]
121
122
 
122
- return "\n".join(summary)
123
+ return "\n".join(summary)
@@ -14,6 +14,7 @@ from basic_memory.mcp.server import mcp
14
14
  from basic_memory.mcp.tools.search import search_notes
15
15
  from basic_memory.mcp.tools.read_note import read_note
16
16
  from basic_memory.schemas.search import SearchResponse
17
+ from basic_memory.config import ConfigManager
17
18
 
18
19
 
19
20
  def _format_search_results_for_chatgpt(results: SearchResponse) -> List[Dict[str, Any]]:
@@ -90,10 +91,14 @@ async def search(
90
91
  logger.info(f"ChatGPT search request: query='{query}'")
91
92
 
92
93
  try:
94
+ # ChatGPT tools don't expose project parameter, so use default project
95
+ config = ConfigManager().config
96
+ default_project = config.default_project
97
+
93
98
  # Call underlying search_notes with sensible defaults for ChatGPT
94
99
  results = await search_notes.fn(
95
100
  query=query,
96
- project=None, # Let project resolution happen automatically
101
+ project=default_project, # Use default project for ChatGPT
97
102
  page=1,
98
103
  page_size=10, # Reasonable default for ChatGPT consumption
99
104
  search_type="text", # Default to full-text search
@@ -149,10 +154,14 @@ async def fetch(
149
154
  logger.info(f"ChatGPT fetch request: id='{id}'")
150
155
 
151
156
  try:
157
+ # ChatGPT tools don't expose project parameter, so use default project
158
+ config = ConfigManager().config
159
+ default_project = config.default_project
160
+
152
161
  # Call underlying read_note function
153
162
  content = await read_note.fn(
154
163
  identifier=id,
155
- project=None, # Let project resolution happen automatically
164
+ project=default_project, # Use default project for ChatGPT
156
165
  page=1,
157
166
  page_size=10, # Default pagination
158
167
  context=context,
@@ -7,7 +7,7 @@ from fastmcp import Context
7
7
  from basic_memory.mcp.project_context import get_active_project
8
8
  from basic_memory.mcp.tools.utils import call_delete
9
9
  from basic_memory.mcp.server import mcp
10
- from basic_memory.mcp.async_client import client
10
+ from basic_memory.mcp.async_client import get_client
11
11
  from basic_memory.schemas import DeleteEntitiesResponse
12
12
 
13
13
 
@@ -202,23 +202,24 @@ async def delete_note(
202
202
  with suggestions for finding the correct identifier, including search
203
203
  commands and alternative formats to try.
204
204
  """
205
- active_project = await get_active_project(client, project, context)
206
- project_url = active_project.project_url
207
-
208
- try:
209
- response = await call_delete(client, f"{project_url}/knowledge/entities/{identifier}")
210
- result = DeleteEntitiesResponse.model_validate(response.json())
211
-
212
- if result.deleted:
213
- logger.info(
214
- f"Successfully deleted note: {identifier} in project: {active_project.name}"
215
- )
216
- return True
217
- else:
218
- logger.warning(f"Delete operation completed but note was not deleted: {identifier}")
219
- return False
220
-
221
- except Exception as e: # pragma: no cover
222
- logger.error(f"Delete failed for '{identifier}': {e}, project: {active_project.name}")
223
- # Return formatted error message for better user experience
224
- return _format_delete_error_response(active_project.name, str(e), identifier)
205
+ async with get_client() as client:
206
+ active_project = await get_active_project(client, project, context)
207
+ project_url = active_project.project_url
208
+
209
+ try:
210
+ response = await call_delete(client, f"{project_url}/knowledge/entities/{identifier}")
211
+ result = DeleteEntitiesResponse.model_validate(response.json())
212
+
213
+ if result.deleted:
214
+ logger.info(
215
+ f"Successfully deleted note: {identifier} in project: {active_project.name}"
216
+ )
217
+ return True
218
+ else:
219
+ logger.warning(f"Delete operation completed but note was not deleted: {identifier}")
220
+ return False
221
+
222
+ except Exception as e: # pragma: no cover
223
+ logger.error(f"Delete failed for '{identifier}': {e}, project: {active_project.name}")
224
+ # Return formatted error message for better user experience
225
+ return _format_delete_error_response(active_project.name, str(e), identifier)
@@ -5,7 +5,7 @@ from typing import Optional
5
5
  from loguru import logger
6
6
  from fastmcp import Context
7
7
 
8
- from basic_memory.mcp.async_client import client
8
+ from basic_memory.mcp.async_client import get_client
9
9
  from basic_memory.mcp.project_context import get_active_project, add_project_metadata
10
10
  from basic_memory.mcp.server import mcp
11
11
  from basic_memory.mcp.tools.utils import call_patch
@@ -214,106 +214,107 @@ async def edit_note(
214
214
  search_notes() first to find the correct identifier. The tool provides detailed
215
215
  error messages with suggestions if operations fail.
216
216
  """
217
- active_project = await get_active_project(client, project, context)
218
- project_url = active_project.project_url
219
-
220
- logger.info("MCP tool call", tool="edit_note", identifier=identifier, operation=operation)
221
-
222
- # Validate operation
223
- valid_operations = ["append", "prepend", "find_replace", "replace_section"]
224
- if operation not in valid_operations:
225
- raise ValueError(
226
- f"Invalid operation '{operation}'. Must be one of: {', '.join(valid_operations)}"
227
- )
228
-
229
- # Validate required parameters for specific operations
230
- if operation == "find_replace" and not find_text:
231
- raise ValueError("find_text parameter is required for find_replace operation")
232
- if operation == "replace_section" and not section:
233
- raise ValueError("section parameter is required for replace_section operation")
234
-
235
- # Use the PATCH endpoint to edit the entity
236
- try:
237
- # Prepare the edit request data
238
- edit_data = {
239
- "operation": operation,
240
- "content": content,
241
- }
242
-
243
- # Add optional parameters
244
- if section:
245
- edit_data["section"] = section
246
- if find_text:
247
- edit_data["find_text"] = find_text
248
- if expected_replacements != 1: # Only send if different from default
249
- edit_data["expected_replacements"] = str(expected_replacements)
250
-
251
- # Call the PATCH endpoint
252
- url = f"{project_url}/knowledge/entities/{identifier}"
253
- response = await call_patch(client, url, json=edit_data)
254
- result = EntityResponse.model_validate(response.json())
255
-
256
- # Format summary
257
- summary = [
258
- f"# Edited note ({operation})",
259
- f"project: {active_project.name}",
260
- f"file_path: {result.file_path}",
261
- f"permalink: {result.permalink}",
262
- f"checksum: {result.checksum[:8] if result.checksum else 'unknown'}",
263
- ]
264
-
265
- # Add operation-specific details
266
- if operation == "append":
267
- lines_added = len(content.split("\n"))
268
- summary.append(f"operation: Added {lines_added} lines to end of note")
269
- elif operation == "prepend":
270
- lines_added = len(content.split("\n"))
271
- summary.append(f"operation: Added {lines_added} lines to beginning of note")
272
- elif operation == "find_replace":
273
- # For find_replace, we can't easily count replacements from here
274
- # since we don't have the original content, but the server handled it
275
- summary.append("operation: Find and replace operation completed")
276
- elif operation == "replace_section":
277
- summary.append(f"operation: Replaced content under section '{section}'")
278
-
279
- # Count observations by category (reuse logic from write_note)
280
- categories = {}
281
- if result.observations:
282
- for obs in result.observations:
283
- categories[obs.category] = categories.get(obs.category, 0) + 1
284
-
285
- summary.append("\\n## Observations")
286
- for category, count in sorted(categories.items()):
287
- summary.append(f"- {category}: {count}")
288
-
289
- # Count resolved/unresolved relations
290
- unresolved = 0
291
- resolved = 0
292
- if result.relations:
293
- unresolved = sum(1 for r in result.relations if not r.to_id)
294
- resolved = len(result.relations) - unresolved
295
-
296
- summary.append("\\n## Relations")
297
- summary.append(f"- Resolved: {resolved}")
298
- if unresolved:
299
- summary.append(f"- Unresolved: {unresolved}")
300
-
301
- logger.info(
302
- "MCP tool response",
303
- tool="edit_note",
304
- operation=operation,
305
- project=active_project.name,
306
- permalink=result.permalink,
307
- observations_count=len(result.observations),
308
- relations_count=len(result.relations),
309
- status_code=response.status_code,
310
- )
311
-
312
- result = "\n".join(summary)
313
- return add_project_metadata(result, active_project.name)
314
-
315
- except Exception as e:
316
- logger.error(f"Error editing note: {e}")
317
- return _format_error_response(
318
- str(e), operation, identifier, find_text, expected_replacements, active_project.name
319
- )
217
+ async with get_client() as client:
218
+ active_project = await get_active_project(client, project, context)
219
+ project_url = active_project.project_url
220
+
221
+ logger.info("MCP tool call", tool="edit_note", identifier=identifier, operation=operation)
222
+
223
+ # Validate operation
224
+ valid_operations = ["append", "prepend", "find_replace", "replace_section"]
225
+ if operation not in valid_operations:
226
+ raise ValueError(
227
+ f"Invalid operation '{operation}'. Must be one of: {', '.join(valid_operations)}"
228
+ )
229
+
230
+ # Validate required parameters for specific operations
231
+ if operation == "find_replace" and not find_text:
232
+ raise ValueError("find_text parameter is required for find_replace operation")
233
+ if operation == "replace_section" and not section:
234
+ raise ValueError("section parameter is required for replace_section operation")
235
+
236
+ # Use the PATCH endpoint to edit the entity
237
+ try:
238
+ # Prepare the edit request data
239
+ edit_data = {
240
+ "operation": operation,
241
+ "content": content,
242
+ }
243
+
244
+ # Add optional parameters
245
+ if section:
246
+ edit_data["section"] = section
247
+ if find_text:
248
+ edit_data["find_text"] = find_text
249
+ if expected_replacements != 1: # Only send if different from default
250
+ edit_data["expected_replacements"] = str(expected_replacements)
251
+
252
+ # Call the PATCH endpoint
253
+ url = f"{project_url}/knowledge/entities/{identifier}"
254
+ response = await call_patch(client, url, json=edit_data)
255
+ result = EntityResponse.model_validate(response.json())
256
+
257
+ # Format summary
258
+ summary = [
259
+ f"# Edited note ({operation})",
260
+ f"project: {active_project.name}",
261
+ f"file_path: {result.file_path}",
262
+ f"permalink: {result.permalink}",
263
+ f"checksum: {result.checksum[:8] if result.checksum else 'unknown'}",
264
+ ]
265
+
266
+ # Add operation-specific details
267
+ if operation == "append":
268
+ lines_added = len(content.split("\n"))
269
+ summary.append(f"operation: Added {lines_added} lines to end of note")
270
+ elif operation == "prepend":
271
+ lines_added = len(content.split("\n"))
272
+ summary.append(f"operation: Added {lines_added} lines to beginning of note")
273
+ elif operation == "find_replace":
274
+ # For find_replace, we can't easily count replacements from here
275
+ # since we don't have the original content, but the server handled it
276
+ summary.append("operation: Find and replace operation completed")
277
+ elif operation == "replace_section":
278
+ summary.append(f"operation: Replaced content under section '{section}'")
279
+
280
+ # Count observations by category (reuse logic from write_note)
281
+ categories = {}
282
+ if result.observations:
283
+ for obs in result.observations:
284
+ categories[obs.category] = categories.get(obs.category, 0) + 1
285
+
286
+ summary.append("\\n## Observations")
287
+ for category, count in sorted(categories.items()):
288
+ summary.append(f"- {category}: {count}")
289
+
290
+ # Count resolved/unresolved relations
291
+ unresolved = 0
292
+ resolved = 0
293
+ if result.relations:
294
+ unresolved = sum(1 for r in result.relations if not r.to_id)
295
+ resolved = len(result.relations) - unresolved
296
+
297
+ summary.append("\\n## Relations")
298
+ summary.append(f"- Resolved: {resolved}")
299
+ if unresolved:
300
+ summary.append(f"- Unresolved: {unresolved}")
301
+
302
+ logger.info(
303
+ "MCP tool response",
304
+ tool="edit_note",
305
+ operation=operation,
306
+ project=active_project.name,
307
+ permalink=result.permalink,
308
+ observations_count=len(result.observations),
309
+ relations_count=len(result.relations),
310
+ status_code=response.status_code,
311
+ )
312
+
313
+ result = "\n".join(summary)
314
+ return add_project_metadata(result, active_project.name)
315
+
316
+ except Exception as e:
317
+ logger.error(f"Error editing note: {e}")
318
+ return _format_error_response(
319
+ str(e), operation, identifier, find_text, expected_replacements, active_project.name
320
+ )