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 List, Union, 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, resolve_project_parameter
10
10
  from basic_memory.mcp.server import mcp
11
11
  from basic_memory.mcp.tools.utils import call_get
@@ -98,162 +98,166 @@ async def recent_activity(
98
98
  - For focused queries, consider using build_context with a specific URI
99
99
  - Max timeframe is 1 year in the past
100
100
  """
101
- # Build common parameters for API calls
102
- params = {
103
- "page": 1,
104
- "page_size": 10,
105
- "max_related": 10,
106
- }
107
- if depth:
108
- params["depth"] = depth
109
- if timeframe:
110
- params["timeframe"] = timeframe # pyright: ignore
111
-
112
- # Validate and convert type parameter
113
- if type:
114
- # Convert single string to list
115
- if isinstance(type, str):
116
- type_list = [type]
117
- else:
118
- type_list = type
119
-
120
- # Validate each type against SearchItemType enum
121
- validated_types = []
122
- for t in type_list:
123
- try:
124
- # Try to convert string to enum
125
- if isinstance(t, str):
126
- validated_types.append(SearchItemType(t.lower()))
127
- except ValueError:
128
- valid_types = [t.value for t in SearchItemType]
129
- raise ValueError(f"Invalid type: {t}. Valid types are: {valid_types}")
130
-
131
- # Add validated types to params
132
- params["type"] = [t.value for t in validated_types] # pyright: ignore
133
-
134
- # Resolve project parameter using the three-tier hierarchy
135
- resolved_project = await resolve_project_parameter(project)
136
-
137
- if resolved_project is None:
138
- # Discovery Mode: Get activity across all projects
139
- logger.info(
140
- f"Getting recent activity across all projects: type={type}, depth={depth}, timeframe={timeframe}"
141
- )
101
+ async with get_client() as client:
102
+ # Build common parameters for API calls
103
+ params = {
104
+ "page": 1,
105
+ "page_size": 10,
106
+ "max_related": 10,
107
+ }
108
+ if depth:
109
+ params["depth"] = depth
110
+ if timeframe:
111
+ params["timeframe"] = timeframe # pyright: ignore
112
+
113
+ # Validate and convert type parameter
114
+ if type:
115
+ # Convert single string to list
116
+ if isinstance(type, str):
117
+ type_list = [type]
118
+ else:
119
+ type_list = type
120
+
121
+ # Validate each type against SearchItemType enum
122
+ validated_types = []
123
+ for t in type_list:
124
+ try:
125
+ # Try to convert string to enum
126
+ if isinstance(t, str):
127
+ validated_types.append(SearchItemType(t.lower()))
128
+ except ValueError:
129
+ valid_types = [t.value for t in SearchItemType]
130
+ raise ValueError(f"Invalid type: {t}. Valid types are: {valid_types}")
131
+
132
+ # Add validated types to params
133
+ params["type"] = [t.value for t in validated_types] # pyright: ignore
134
+
135
+ # Resolve project parameter using the three-tier hierarchy
136
+ resolved_project = await resolve_project_parameter(project)
137
+
138
+ if resolved_project is None:
139
+ # Discovery Mode: Get activity across all projects
140
+ logger.info(
141
+ f"Getting recent activity across all projects: type={type}, depth={depth}, timeframe={timeframe}"
142
+ )
142
143
 
143
- # Get list of all projects
144
- response = await call_get(client, "/projects/projects")
145
- project_list = ProjectList.model_validate(response.json())
146
-
147
- projects_activity = {}
148
- total_items = 0
149
- total_entities = 0
150
- total_relations = 0
151
- total_observations = 0
152
- most_active_project = None
153
- most_active_count = 0
154
- active_projects = 0
155
-
156
- # Query each project's activity
157
- for project_info in project_list.projects:
158
- project_activity = await _get_project_activity(client, project_info, params, depth)
159
- projects_activity[project_info.name] = project_activity
160
-
161
- # Aggregate stats
162
- item_count = project_activity.item_count
163
- if item_count > 0:
164
- active_projects += 1
165
- total_items += item_count
166
-
167
- # Count by type
168
- for result in project_activity.activity.results:
169
- if result.primary_result.type == "entity":
170
- total_entities += 1
171
- elif result.primary_result.type == "relation":
172
- total_relations += 1
173
- elif result.primary_result.type == "observation":
174
- total_observations += 1
175
-
176
- # Track most active project
177
- if item_count > most_active_count:
178
- most_active_count = item_count
179
- most_active_project = project_info.name
180
-
181
- # Build summary stats
182
- summary = ActivityStats(
183
- total_projects=len(project_list.projects),
184
- active_projects=active_projects,
185
- most_active_project=most_active_project,
186
- total_items=total_items,
187
- total_entities=total_entities,
188
- total_relations=total_relations,
189
- total_observations=total_observations,
190
- )
144
+ # Get list of all projects
145
+ response = await call_get(client, "/projects/projects")
146
+ project_list = ProjectList.model_validate(response.json())
147
+
148
+ projects_activity = {}
149
+ total_items = 0
150
+ total_entities = 0
151
+ total_relations = 0
152
+ total_observations = 0
153
+ most_active_project = None
154
+ most_active_count = 0
155
+ active_projects = 0
156
+
157
+ # Query each project's activity
158
+ for project_info in project_list.projects:
159
+ project_activity = await _get_project_activity(client, project_info, params, depth)
160
+ projects_activity[project_info.name] = project_activity
161
+
162
+ # Aggregate stats
163
+ item_count = project_activity.item_count
164
+ if item_count > 0:
165
+ active_projects += 1
166
+ total_items += item_count
167
+
168
+ # Count by type
169
+ for result in project_activity.activity.results:
170
+ if result.primary_result.type == "entity":
171
+ total_entities += 1
172
+ elif result.primary_result.type == "relation":
173
+ total_relations += 1
174
+ elif result.primary_result.type == "observation":
175
+ total_observations += 1
176
+
177
+ # Track most active project
178
+ if item_count > most_active_count:
179
+ most_active_count = item_count
180
+ most_active_project = project_info.name
181
+
182
+ # Build summary stats
183
+ summary = ActivityStats(
184
+ total_projects=len(project_list.projects),
185
+ active_projects=active_projects,
186
+ most_active_project=most_active_project,
187
+ total_items=total_items,
188
+ total_entities=total_entities,
189
+ total_relations=total_relations,
190
+ total_observations=total_observations,
191
+ )
191
192
 
192
- # Generate guidance for the assistant
193
- guidance_lines = ["\n" + "─" * 40]
193
+ # Generate guidance for the assistant
194
+ guidance_lines = ["\n" + "─" * 40]
194
195
 
195
- if most_active_project and most_active_count > 0:
196
- guidance_lines.extend(
197
- [
198
- f"Suggested project: '{most_active_project}' (most active with {most_active_count} items)",
199
- f"Ask user: 'Should I use {most_active_project} for this task, or would you prefer a different project?'",
200
- ]
201
- )
202
- elif active_projects > 0:
203
- # Has activity but no clear most active project
204
- active_project_names = [
205
- name for name, activity in projects_activity.items() if activity.item_count > 0
206
- ]
207
- if len(active_project_names) == 1:
196
+ if most_active_project and most_active_count > 0:
208
197
  guidance_lines.extend(
209
198
  [
210
- f"Suggested project: '{active_project_names[0]}' (only active project)",
211
- f"Ask user: 'Should I use {active_project_names[0]} for this task?'",
199
+ f"Suggested project: '{most_active_project}' (most active with {most_active_count} items)",
200
+ f"Ask user: 'Should I use {most_active_project} for this task, or would you prefer a different project?'",
212
201
  ]
213
202
  )
203
+ elif active_projects > 0:
204
+ # Has activity but no clear most active project
205
+ active_project_names = [
206
+ name for name, activity in projects_activity.items() if activity.item_count > 0
207
+ ]
208
+ if len(active_project_names) == 1:
209
+ guidance_lines.extend(
210
+ [
211
+ f"Suggested project: '{active_project_names[0]}' (only active project)",
212
+ f"Ask user: 'Should I use {active_project_names[0]} for this task?'",
213
+ ]
214
+ )
215
+ else:
216
+ guidance_lines.extend(
217
+ [
218
+ f"Multiple active projects found: {', '.join(active_project_names)}",
219
+ "Ask user: 'Which project should I use for this task?'",
220
+ ]
221
+ )
214
222
  else:
223
+ # No recent activity
215
224
  guidance_lines.extend(
216
225
  [
217
- f"Multiple active projects found: {', '.join(active_project_names)}",
218
- "Ask user: 'Which project should I use for this task?'",
226
+ "No recent activity found in any project.",
227
+ "Consider: Ask which project to use or if they want to create a new one.",
219
228
  ]
220
229
  )
221
- else:
222
- # No recent activity
230
+
223
231
  guidance_lines.extend(
224
232
  [
225
- "No recent activity found in any project.",
226
- "Consider: Ask which project to use or if they want to create a new one.",
233
+ "",
234
+ "Session reminder: Remember their project choice throughout this conversation.",
227
235
  ]
228
236
  )
229
237
 
230
- guidance_lines.extend(
231
- ["", "Session reminder: Remember their project choice throughout this conversation."]
232
- )
233
-
234
- guidance = "\n".join(guidance_lines)
238
+ guidance = "\n".join(guidance_lines)
235
239
 
236
- # Format discovery mode output
237
- return _format_discovery_output(projects_activity, summary, timeframe, guidance)
240
+ # Format discovery mode output
241
+ return _format_discovery_output(projects_activity, summary, timeframe, guidance)
238
242
 
239
- else:
240
- # Project-Specific Mode: Get activity for specific project
241
- logger.info(
242
- f"Getting recent activity from project {resolved_project}: type={type}, depth={depth}, timeframe={timeframe}"
243
- )
243
+ else:
244
+ # Project-Specific Mode: Get activity for specific project
245
+ logger.info(
246
+ f"Getting recent activity from project {resolved_project}: type={type}, depth={depth}, timeframe={timeframe}"
247
+ )
244
248
 
245
- active_project = await get_active_project(client, resolved_project, context)
246
- project_url = active_project.project_url
249
+ active_project = await get_active_project(client, resolved_project, context)
250
+ project_url = active_project.project_url
247
251
 
248
- response = await call_get(
249
- client,
250
- f"{project_url}/memory/recent",
251
- params=params,
252
- )
253
- activity_data = GraphContext.model_validate(response.json())
252
+ response = await call_get(
253
+ client,
254
+ f"{project_url}/memory/recent",
255
+ params=params,
256
+ )
257
+ activity_data = GraphContext.model_validate(response.json())
254
258
 
255
- # Format project-specific mode output
256
- return _format_project_output(resolved_project, activity_data, timeframe, type)
259
+ # Format project-specific mode output
260
+ return _format_project_output(resolved_project, activity_data, timeframe, type)
257
261
 
258
262
 
259
263
  async def _get_project_activity(
@@ -6,7 +6,7 @@ from typing import List, Optional
6
6
  from loguru import logger
7
7
  from fastmcp import Context
8
8
 
9
- from basic_memory.mcp.async_client import client
9
+ from basic_memory.mcp.async_client import get_client
10
10
  from basic_memory.mcp.project_context import get_active_project
11
11
  from basic_memory.mcp.server import mcp
12
12
  from basic_memory.mcp.tools.utils import call_post
@@ -353,31 +353,32 @@ async def search_notes(
353
353
  if after_date:
354
354
  search_query.after_date = after_date
355
355
 
356
- active_project = await get_active_project(client, project, context)
357
- project_url = active_project.project_url
356
+ async with get_client() as client:
357
+ active_project = await get_active_project(client, project, context)
358
+ project_url = active_project.project_url
358
359
 
359
- logger.info(f"Searching for {search_query} in project {active_project.name}")
360
+ logger.info(f"Searching for {search_query} in project {active_project.name}")
360
361
 
361
- try:
362
- response = await call_post(
363
- client,
364
- f"{project_url}/search/",
365
- json=search_query.model_dump(),
366
- params={"page": page, "page_size": page_size},
367
- )
368
- result = SearchResponse.model_validate(response.json())
369
-
370
- # Check if we got no results and provide helpful guidance
371
- if not result.results:
372
- logger.info(
373
- f"Search returned no results for query: {query} in project {active_project.name}"
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},
374
368
  )
375
- # Don't treat this as an error, but the user might want guidance
376
- # We return the empty result as normal - the user can decide if they need help
377
-
378
- return result
379
-
380
- except Exception as e:
381
- logger.error(f"Search failed for query '{query}': {e}, project: {active_project.name}")
382
- # Return formatted error message as string for better user experience
383
- return _format_search_error_response(active_project.name, str(e), query, search_type)
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)