basic-memory 0.12.3__py3-none-any.whl → 0.13.0b2__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 (107) hide show
  1. basic_memory/__init__.py +7 -1
  2. basic_memory/alembic/env.py +1 -1
  3. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  4. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -5
  5. basic_memory/api/app.py +43 -13
  6. basic_memory/api/routers/__init__.py +4 -2
  7. basic_memory/api/routers/directory_router.py +63 -0
  8. basic_memory/api/routers/importer_router.py +152 -0
  9. basic_memory/api/routers/knowledge_router.py +127 -38
  10. basic_memory/api/routers/management_router.py +78 -0
  11. basic_memory/api/routers/memory_router.py +4 -59
  12. basic_memory/api/routers/project_router.py +230 -0
  13. basic_memory/api/routers/prompt_router.py +260 -0
  14. basic_memory/api/routers/search_router.py +3 -21
  15. basic_memory/api/routers/utils.py +130 -0
  16. basic_memory/api/template_loader.py +292 -0
  17. basic_memory/cli/app.py +20 -21
  18. basic_memory/cli/commands/__init__.py +2 -1
  19. basic_memory/cli/commands/auth.py +136 -0
  20. basic_memory/cli/commands/db.py +3 -3
  21. basic_memory/cli/commands/import_chatgpt.py +31 -207
  22. basic_memory/cli/commands/import_claude_conversations.py +16 -142
  23. basic_memory/cli/commands/import_claude_projects.py +33 -143
  24. basic_memory/cli/commands/import_memory_json.py +26 -83
  25. basic_memory/cli/commands/mcp.py +71 -18
  26. basic_memory/cli/commands/project.py +99 -67
  27. basic_memory/cli/commands/status.py +19 -9
  28. basic_memory/cli/commands/sync.py +44 -58
  29. basic_memory/cli/main.py +1 -5
  30. basic_memory/config.py +144 -88
  31. basic_memory/db.py +6 -4
  32. basic_memory/deps.py +227 -30
  33. basic_memory/importers/__init__.py +27 -0
  34. basic_memory/importers/base.py +79 -0
  35. basic_memory/importers/chatgpt_importer.py +222 -0
  36. basic_memory/importers/claude_conversations_importer.py +172 -0
  37. basic_memory/importers/claude_projects_importer.py +148 -0
  38. basic_memory/importers/memory_json_importer.py +93 -0
  39. basic_memory/importers/utils.py +58 -0
  40. basic_memory/markdown/entity_parser.py +5 -2
  41. basic_memory/mcp/auth_provider.py +270 -0
  42. basic_memory/mcp/external_auth_provider.py +321 -0
  43. basic_memory/mcp/project_session.py +103 -0
  44. basic_memory/mcp/prompts/continue_conversation.py +18 -68
  45. basic_memory/mcp/prompts/recent_activity.py +19 -3
  46. basic_memory/mcp/prompts/search.py +14 -140
  47. basic_memory/mcp/prompts/utils.py +3 -3
  48. basic_memory/mcp/{tools → resources}/project_info.py +6 -2
  49. basic_memory/mcp/server.py +82 -8
  50. basic_memory/mcp/supabase_auth_provider.py +463 -0
  51. basic_memory/mcp/tools/__init__.py +20 -0
  52. basic_memory/mcp/tools/build_context.py +11 -1
  53. basic_memory/mcp/tools/canvas.py +15 -2
  54. basic_memory/mcp/tools/delete_note.py +12 -4
  55. basic_memory/mcp/tools/edit_note.py +297 -0
  56. basic_memory/mcp/tools/list_directory.py +154 -0
  57. basic_memory/mcp/tools/move_note.py +87 -0
  58. basic_memory/mcp/tools/project_management.py +300 -0
  59. basic_memory/mcp/tools/read_content.py +15 -6
  60. basic_memory/mcp/tools/read_note.py +17 -5
  61. basic_memory/mcp/tools/recent_activity.py +11 -2
  62. basic_memory/mcp/tools/search.py +10 -1
  63. basic_memory/mcp/tools/utils.py +137 -12
  64. basic_memory/mcp/tools/write_note.py +11 -15
  65. basic_memory/models/__init__.py +3 -2
  66. basic_memory/models/knowledge.py +16 -4
  67. basic_memory/models/project.py +80 -0
  68. basic_memory/models/search.py +8 -5
  69. basic_memory/repository/__init__.py +2 -0
  70. basic_memory/repository/entity_repository.py +8 -3
  71. basic_memory/repository/observation_repository.py +35 -3
  72. basic_memory/repository/project_info_repository.py +3 -2
  73. basic_memory/repository/project_repository.py +85 -0
  74. basic_memory/repository/relation_repository.py +8 -2
  75. basic_memory/repository/repository.py +107 -15
  76. basic_memory/repository/search_repository.py +87 -27
  77. basic_memory/schemas/__init__.py +6 -0
  78. basic_memory/schemas/directory.py +30 -0
  79. basic_memory/schemas/importer.py +34 -0
  80. basic_memory/schemas/memory.py +26 -12
  81. basic_memory/schemas/project_info.py +112 -2
  82. basic_memory/schemas/prompt.py +90 -0
  83. basic_memory/schemas/request.py +56 -2
  84. basic_memory/schemas/search.py +1 -1
  85. basic_memory/services/__init__.py +2 -1
  86. basic_memory/services/context_service.py +208 -95
  87. basic_memory/services/directory_service.py +167 -0
  88. basic_memory/services/entity_service.py +385 -5
  89. basic_memory/services/exceptions.py +6 -0
  90. basic_memory/services/file_service.py +14 -15
  91. basic_memory/services/initialization.py +144 -67
  92. basic_memory/services/link_resolver.py +16 -8
  93. basic_memory/services/project_service.py +548 -0
  94. basic_memory/services/search_service.py +77 -2
  95. basic_memory/sync/background_sync.py +25 -0
  96. basic_memory/sync/sync_service.py +10 -9
  97. basic_memory/sync/watch_service.py +63 -39
  98. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  99. basic_memory/templates/prompts/search.hbs +101 -0
  100. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/METADATA +23 -1
  101. basic_memory-0.13.0b2.dist-info/RECORD +132 -0
  102. basic_memory/api/routers/project_info_router.py +0 -274
  103. basic_memory/mcp/main.py +0 -24
  104. basic_memory-0.12.3.dist-info/RECORD +0 -100
  105. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/WHEEL +0 -0
  106. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/entry_points.txt +0 -0
  107. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,300 @@
1
+ """Project management tools for Basic Memory MCP server.
2
+
3
+ These tools allow users to switch between projects, list available projects,
4
+ and manage project context during conversations.
5
+ """
6
+
7
+ from fastmcp import Context
8
+ from loguru import logger
9
+
10
+ from basic_memory.config import get_project_config
11
+ from basic_memory.mcp.async_client import client
12
+ from basic_memory.mcp.project_session import session, add_project_metadata
13
+ from basic_memory.mcp.server import mcp
14
+ from basic_memory.mcp.tools.utils import call_get, call_put, call_post, call_delete
15
+ from basic_memory.schemas import ProjectInfoResponse
16
+ from basic_memory.schemas.project_info import ProjectList, ProjectStatusResponse, ProjectInfoRequest
17
+
18
+
19
+ @mcp.tool()
20
+ async def list_projects(ctx: Context | None = None) -> str:
21
+ """List all available projects with their status.
22
+
23
+ Shows all Basic Memory projects that are available, indicating which one
24
+ is currently active and which is the default.
25
+
26
+ Returns:
27
+ Formatted list of projects with status indicators
28
+
29
+ Example:
30
+ list_projects()
31
+ """
32
+ if ctx: # pragma: no cover
33
+ await ctx.info("Listing all available projects")
34
+
35
+ # Get projects from API
36
+ response = await call_get(client, "/projects/projects")
37
+ project_list = ProjectList.model_validate(response.json())
38
+
39
+ current = session.get_current_project()
40
+
41
+ result = "Available projects:\n"
42
+
43
+ for project in project_list.projects:
44
+ indicators = []
45
+ if project.name == current:
46
+ indicators.append("current")
47
+ if project.is_default:
48
+ indicators.append("default")
49
+
50
+ if indicators:
51
+ result += f"• {project.name} ({', '.join(indicators)})\n"
52
+ else:
53
+ result += f"• {project.name}\n"
54
+
55
+ return add_project_metadata(result, current)
56
+
57
+
58
+ @mcp.tool()
59
+ async def switch_project(project_name: str, ctx: Context | None = None) -> str:
60
+ """Switch to a different project context.
61
+
62
+ Changes the active project context for all subsequent tool calls.
63
+ Shows a project summary after switching successfully.
64
+
65
+ Args:
66
+ project_name: Name of the project to switch to
67
+
68
+ Returns:
69
+ Confirmation message with project summary
70
+
71
+ Example:
72
+ switch_project("work-notes")
73
+ switch_project("personal-journal")
74
+ """
75
+ if ctx: # pragma: no cover
76
+ await ctx.info(f"Switching to project: {project_name}")
77
+
78
+ current_project = session.get_current_project()
79
+ try:
80
+ # Validate project exists by getting project list
81
+ response = await call_get(client, "/projects/projects")
82
+ project_list = ProjectList.model_validate(response.json())
83
+
84
+ # Check if project exists
85
+ project_exists = any(p.name == project_name for p in project_list.projects)
86
+ if not project_exists:
87
+ available_projects = [p.name for p in project_list.projects]
88
+ return f"Error: Project '{project_name}' not found. Available projects: {', '.join(available_projects)}"
89
+
90
+ # Switch to the project
91
+ session.set_current_project(project_name)
92
+ current_project = session.get_current_project()
93
+ project_config = get_project_config(current_project)
94
+
95
+ # Get project info to show summary
96
+ try:
97
+ response = await call_get(client, f"{project_config.project_url}/project/info")
98
+ project_info = ProjectInfoResponse.model_validate(response.json())
99
+
100
+ result = f"✓ Switched to {project_name} project\n\n"
101
+ result += "Project Summary:\n"
102
+ result += f"• {project_info.statistics.total_entities} entities\n"
103
+ result += f"• {project_info.statistics.total_observations} observations\n"
104
+ result += f"• {project_info.statistics.total_relations} relations\n"
105
+
106
+ except Exception as e:
107
+ # If we can't get project info, still confirm the switch
108
+ logger.warning(f"Could not get project info for {project_name}: {e}")
109
+ result = f"✓ Switched to {project_name} project\n\n"
110
+ result += "Project summary unavailable.\n"
111
+
112
+ return add_project_metadata(result, project_name)
113
+
114
+ except Exception as e:
115
+ logger.error(f"Error switching to project {project_name}: {e}")
116
+ # Revert to previous project on error
117
+ session.set_current_project(current_project)
118
+ raise e
119
+
120
+
121
+ @mcp.tool()
122
+ async def get_current_project(ctx: Context | None = None) -> str:
123
+ """Show the currently active project and basic stats.
124
+
125
+ Displays which project is currently active and provides basic information
126
+ about it.
127
+
128
+ Returns:
129
+ Current project name and basic statistics
130
+
131
+ Example:
132
+ get_current_project()
133
+ """
134
+ if ctx: # pragma: no cover
135
+ await ctx.info("Getting current project information")
136
+
137
+ current_project = session.get_current_project()
138
+ project_config = get_project_config(current_project)
139
+ result = f"Current project: {current_project}\n\n"
140
+
141
+ # get project stats
142
+ response = await call_get(client, f"{project_config.project_url}/project/info")
143
+ project_info = ProjectInfoResponse.model_validate(response.json())
144
+
145
+ result += f"• {project_info.statistics.total_entities} entities\n"
146
+ result += f"• {project_info.statistics.total_observations} observations\n"
147
+ result += f"• {project_info.statistics.total_relations} relations\n"
148
+
149
+ default_project = session.get_default_project()
150
+ if current_project != default_project:
151
+ result += f"• Default project: {default_project}\n"
152
+
153
+ return add_project_metadata(result, current_project)
154
+
155
+
156
+ @mcp.tool()
157
+ async def set_default_project(project_name: str, ctx: Context | None = None) -> str:
158
+ """Set default project in config. Requires restart to take effect.
159
+
160
+ Updates the configuration to use a different default project. This change
161
+ only takes effect after restarting the Basic Memory server.
162
+
163
+ Args:
164
+ project_name: Name of the project to set as default
165
+
166
+ Returns:
167
+ Confirmation message about config update
168
+
169
+ Example:
170
+ set_default_project("work-notes")
171
+ """
172
+ if ctx: # pragma: no cover
173
+ await ctx.info(f"Setting default project to: {project_name}")
174
+
175
+ # Call API to set default project
176
+ response = await call_put(client, f"/projects/{project_name}/default")
177
+ status_response = ProjectStatusResponse.model_validate(response.json())
178
+
179
+ result = f"✓ {status_response.message}\n\n"
180
+ result += "Restart Basic Memory for this change to take effect:\n"
181
+ result += "basic-memory mcp\n"
182
+
183
+ if status_response.old_project:
184
+ result += f"\nPrevious default: {status_response.old_project.name}\n"
185
+
186
+ return add_project_metadata(result, session.get_current_project())
187
+
188
+
189
+ @mcp.tool()
190
+ async def create_project(
191
+ project_name: str, project_path: str, set_default: bool = False, ctx: Context | None = None
192
+ ) -> str:
193
+ """Create a new Basic Memory project.
194
+
195
+ Creates a new project with the specified name and path. The project directory
196
+ will be created if it doesn't exist. Optionally sets the new project as default.
197
+
198
+ Args:
199
+ project_name: Name for the new project (must be unique)
200
+ project_path: File system path where the project will be stored
201
+ set_default: Whether to set this project as the default (optional, defaults to False)
202
+
203
+ Returns:
204
+ Confirmation message with project details
205
+
206
+ Example:
207
+ create_project("my-research", "~/Documents/research")
208
+ create_project("work-notes", "/home/user/work", set_default=True)
209
+ """
210
+ if ctx: # pragma: no cover
211
+ await ctx.info(f"Creating project: {project_name} at {project_path}")
212
+
213
+ # Create the project request
214
+ project_request = ProjectInfoRequest(
215
+ name=project_name, path=project_path, set_default=set_default
216
+ )
217
+
218
+ # Call API to create project
219
+ response = await call_post(client, "/projects/projects", json=project_request.model_dump())
220
+ status_response = ProjectStatusResponse.model_validate(response.json())
221
+
222
+ result = f"✓ {status_response.message}\n\n"
223
+
224
+ if status_response.new_project:
225
+ result += "Project Details:\n"
226
+ result += f"• Name: {status_response.new_project.name}\n"
227
+ result += f"• Path: {status_response.new_project.path}\n"
228
+
229
+ if set_default:
230
+ result += "• Set as default project\n"
231
+
232
+ result += "\nProject is now available for use.\n"
233
+
234
+ # If project was set as default, update session
235
+ if set_default:
236
+ session.set_current_project(project_name)
237
+
238
+ return add_project_metadata(result, session.get_current_project())
239
+
240
+
241
+ @mcp.tool()
242
+ async def delete_project(project_name: str, ctx: Context | None = None) -> str:
243
+ """Delete a Basic Memory project.
244
+
245
+ Removes a project from the configuration and database. This does NOT delete
246
+ the actual files on disk - only removes the project from Basic Memory's
247
+ configuration and database records.
248
+
249
+ Args:
250
+ project_name: Name of the project to delete
251
+
252
+ Returns:
253
+ Confirmation message about project deletion
254
+
255
+ Example:
256
+ delete_project("old-project")
257
+
258
+ Warning:
259
+ This action cannot be undone. The project will need to be re-added
260
+ to access its content through Basic Memory again.
261
+ """
262
+ if ctx: # pragma: no cover
263
+ await ctx.info(f"Deleting project: {project_name}")
264
+
265
+ current_project = session.get_current_project()
266
+
267
+ # Check if trying to delete current project
268
+ if project_name == current_project:
269
+ raise ValueError(
270
+ f"Cannot delete the currently active project '{project_name}'. Switch to a different project first."
271
+ )
272
+
273
+ # Get project info before deletion to validate it exists
274
+ response = await call_get(client, "/projects/projects")
275
+ project_list = ProjectList.model_validate(response.json())
276
+
277
+ # Check if project exists
278
+ project_exists = any(p.name == project_name for p in project_list.projects)
279
+ if not project_exists:
280
+ available_projects = [p.name for p in project_list.projects]
281
+ raise ValueError(
282
+ f"Project '{project_name}' not found. Available projects: {', '.join(available_projects)}"
283
+ )
284
+
285
+ # Call API to delete project
286
+ response = await call_delete(client, f"/projects/{project_name}")
287
+ status_response = ProjectStatusResponse.model_validate(response.json())
288
+
289
+ result = f"✓ {status_response.message}\n\n"
290
+
291
+ if status_response.old_project:
292
+ result += "Removed project details:\n"
293
+ result += f"• Name: {status_response.old_project.name}\n"
294
+ if hasattr(status_response.old_project, "path"):
295
+ result += f"• Path: {status_response.old_project.path}\n"
296
+
297
+ result += "Files remain on disk but project is no longer tracked by Basic Memory.\n"
298
+ result += "Re-add the project to access its content again.\n"
299
+
300
+ return add_project_metadata(result, session.get_current_project())
@@ -5,17 +5,19 @@ supporting various file types including text, images, and other binary files.
5
5
  Files are read directly without any knowledge graph processing.
6
6
  """
7
7
 
8
+ from typing import Optional
9
+ import base64
10
+ import io
11
+
8
12
  from loguru import logger
13
+ from PIL import Image as PILImage
9
14
 
10
15
  from basic_memory.mcp.server import mcp
11
16
  from basic_memory.mcp.async_client import client
12
17
  from basic_memory.mcp.tools.utils import call_get
18
+ from basic_memory.mcp.project_session import get_active_project
13
19
  from basic_memory.schemas.memory import memory_url_path
14
20
 
15
- import base64
16
- import io
17
- from PIL import Image as PILImage
18
-
19
21
 
20
22
  def calculate_target_params(content_length):
21
23
  """Calculate initial quality and size based on input file size"""
@@ -144,7 +146,7 @@ def optimize_image(img, content_length, max_output_bytes=350000):
144
146
 
145
147
 
146
148
  @mcp.tool(description="Read a file's raw content by path or permalink")
147
- async def read_content(path: str) -> dict:
149
+ async def read_content(path: str, project: Optional[str] = None) -> dict:
148
150
  """Read a file's raw content by path or permalink.
149
151
 
150
152
  This tool provides direct access to file content in the knowledge base,
@@ -158,6 +160,7 @@ async def read_content(path: str) -> dict:
158
160
  - A regular file path (docs/example.md)
159
161
  - A memory URL (memory://docs/example)
160
162
  - A permalink (docs/example)
163
+ project: Optional project name to read from. If not provided, uses current active project.
161
164
 
162
165
  Returns:
163
166
  A dictionary with the file content and metadata:
@@ -175,11 +178,17 @@ async def read_content(path: str) -> dict:
175
178
 
176
179
  # Read using memory URL
177
180
  content = await read_file("memory://docs/architecture")
181
+
182
+ # Read from specific project
183
+ content = await read_content("docs/example.md", project="work-project")
178
184
  """
179
185
  logger.info("Reading file", path=path)
180
186
 
187
+ active_project = get_active_project(project)
188
+ project_url = active_project.project_url
189
+
181
190
  url = memory_url_path(path)
182
- response = await call_get(client, f"/resource/{url}")
191
+ response = await call_get(client, f"{project_url}/resource/{url}")
183
192
  content_type = response.headers.get("content-type", "application/octet-stream")
184
193
  content_length = int(response.headers.get("content-length", 0))
185
194
 
@@ -1,6 +1,7 @@
1
1
  """Read note tool for Basic Memory MCP server."""
2
2
 
3
3
  from textwrap import dedent
4
+ from typing import Optional
4
5
 
5
6
  from loguru import logger
6
7
 
@@ -8,13 +9,16 @@ from basic_memory.mcp.async_client import client
8
9
  from basic_memory.mcp.server import mcp
9
10
  from basic_memory.mcp.tools.search import search_notes
10
11
  from basic_memory.mcp.tools.utils import call_get
12
+ from basic_memory.mcp.project_session import get_active_project
11
13
  from basic_memory.schemas.memory import memory_url_path
12
14
 
13
15
 
14
16
  @mcp.tool(
15
17
  description="Read a markdown note by title or permalink.",
16
18
  )
17
- async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
19
+ async def read_note(
20
+ identifier: str, page: int = 1, page_size: int = 10, project: Optional[str] = None
21
+ ) -> str:
18
22
  """Read a markdown note from the knowledge base.
19
23
 
20
24
  This tool finds and retrieves a note by its title, permalink, or content search,
@@ -26,6 +30,7 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
26
30
  Can be a full memory:// URL, a permalink, a title, or search text
27
31
  page: Page number for paginated results (default: 1)
28
32
  page_size: Number of items per page (default: 10)
33
+ project: Optional project name to read from. If not provided, uses current active project.
29
34
 
30
35
  Returns:
31
36
  The full markdown content of the note if found, or helpful guidance if not found.
@@ -42,10 +47,17 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
42
47
 
43
48
  # Read with pagination
44
49
  read_note("Project Updates", page=2, page_size=5)
50
+
51
+ # Read from specific project
52
+ read_note("Meeting Notes", project="work-project")
45
53
  """
54
+
55
+ active_project = get_active_project(project)
56
+ project_url = active_project.project_url
57
+
46
58
  # Get the file via REST API - first try direct permalink lookup
47
59
  entity_path = memory_url_path(identifier)
48
- path = f"/resource/{entity_path}"
60
+ path = f"{project_url}/resource/{entity_path}"
49
61
  logger.info(f"Attempting to read note from URL: {path}")
50
62
 
51
63
  try:
@@ -62,14 +74,14 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
62
74
 
63
75
  # Fallback 1: Try title search via API
64
76
  logger.info(f"Search title for: {identifier}")
65
- title_results = await search_notes(query=identifier, search_type="title")
77
+ title_results = await search_notes(query=identifier, search_type="title", project=project)
66
78
 
67
79
  if title_results and title_results.results:
68
80
  result = title_results.results[0] # Get the first/best match
69
81
  if result.permalink:
70
82
  try:
71
83
  # Try to fetch the content using the found permalink
72
- path = f"/resource/{result.permalink}"
84
+ path = f"{project_url}/resource/{result.permalink}"
73
85
  response = await call_get(
74
86
  client, path, params={"page": page, "page_size": page_size}
75
87
  )
@@ -86,7 +98,7 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
86
98
 
87
99
  # Fallback 2: Text search as a last resort
88
100
  logger.info(f"Title search failed, trying text search for: {identifier}")
89
- text_results = await search_notes(query=identifier, search_type="text")
101
+ text_results = await search_notes(query=identifier, search_type="text", project=project)
90
102
 
91
103
  # We didn't find a direct match, construct a helpful error message
92
104
  if not text_results or not text_results.results:
@@ -1,12 +1,13 @@
1
1
  """Recent activity tool for Basic Memory MCP server."""
2
2
 
3
- from typing import List, Union
3
+ from typing import List, Union, Optional
4
4
 
5
5
  from loguru import logger
6
6
 
7
7
  from basic_memory.mcp.async_client import client
8
8
  from basic_memory.mcp.server import mcp
9
9
  from basic_memory.mcp.tools.utils import call_get
10
+ from basic_memory.mcp.project_session import get_active_project
10
11
  from basic_memory.schemas.base import TimeFrame
11
12
  from basic_memory.schemas.memory import GraphContext
12
13
  from basic_memory.schemas.search import SearchItemType
@@ -31,6 +32,7 @@ async def recent_activity(
31
32
  page: int = 1,
32
33
  page_size: int = 10,
33
34
  max_related: int = 10,
35
+ project: Optional[str] = None,
34
36
  ) -> GraphContext:
35
37
  """Get recent activity across the knowledge base.
36
38
 
@@ -51,6 +53,7 @@ async def recent_activity(
51
53
  page: Page number of results to return (default: 1)
52
54
  page_size: Number of results to return per page (default: 10)
53
55
  max_related: Maximum number of related results to return (default: 10)
56
+ project: Optional project name to get activity from. If not provided, uses current active project.
54
57
 
55
58
  Returns:
56
59
  GraphContext containing:
@@ -74,6 +77,9 @@ async def recent_activity(
74
77
  # Look back further with more context
75
78
  recent_activity(type="entity", depth=2, timeframe="2 weeks ago")
76
79
 
80
+ # Get activity from specific project
81
+ recent_activity(type="entity", project="work-project")
82
+
77
83
  Notes:
78
84
  - Higher depth values (>3) may impact performance with large result sets
79
85
  - For focused queries, consider using build_context with a specific URI
@@ -114,9 +120,12 @@ async def recent_activity(
114
120
  # Add validated types to params
115
121
  params["type"] = [t.value for t in validated_types] # pyright: ignore
116
122
 
123
+ active_project = get_active_project(project)
124
+ project_url = active_project.project_url
125
+
117
126
  response = await call_get(
118
127
  client,
119
- "/memory/recent",
128
+ f"{project_url}/memory/recent",
120
129
  params=params,
121
130
  )
122
131
  return GraphContext.model_validate(response.json())
@@ -7,6 +7,7 @@ from loguru import logger
7
7
  from basic_memory.mcp.async_client import client
8
8
  from basic_memory.mcp.server import mcp
9
9
  from basic_memory.mcp.tools.utils import call_post
10
+ from basic_memory.mcp.project_session import get_active_project
10
11
  from basic_memory.schemas.search import SearchItemType, SearchQuery, SearchResponse
11
12
 
12
13
 
@@ -21,6 +22,7 @@ async def search_notes(
21
22
  types: Optional[List[str]] = None,
22
23
  entity_types: Optional[List[str]] = None,
23
24
  after_date: Optional[str] = None,
25
+ project: Optional[str] = None,
24
26
  ) -> SearchResponse:
25
27
  """Search across all content in the knowledge base.
26
28
 
@@ -36,6 +38,7 @@ async def search_notes(
36
38
  types: Optional list of note types to search (e.g., ["note", "person"])
37
39
  entity_types: Optional list of entity types to filter by (e.g., ["entity", "observation"])
38
40
  after_date: Optional date filter for recent content (e.g., "1 week", "2d")
41
+ project: Optional project name to search in. If not provided, uses current active project.
39
42
 
40
43
  Returns:
41
44
  SearchResponse with results and pagination info
@@ -79,6 +82,9 @@ async def search_notes(
79
82
  query="docs/meeting-*",
80
83
  search_type="permalink"
81
84
  )
85
+
86
+ # Search in specific project
87
+ results = await search_notes("meeting notes", project="work-project")
82
88
  """
83
89
  # Create a SearchQuery object based on the parameters
84
90
  search_query = SearchQuery()
@@ -103,10 +109,13 @@ async def search_notes(
103
109
  if after_date:
104
110
  search_query.after_date = after_date
105
111
 
112
+ active_project = get_active_project(project)
113
+ project_url = active_project.project_url
114
+
106
115
  logger.info(f"Searching for {search_query}")
107
116
  response = await call_post(
108
117
  client,
109
- "/search/",
118
+ f"{project_url}/search/",
110
119
  json=search_query.model_dump(),
111
120
  params={"page": page, "page_size": page_size},
112
121
  )