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,242 +4,75 @@ These tools allow users to switch between projects, list available projects,
4
4
  and manage project context during conversations.
5
5
  """
6
6
 
7
- from textwrap import dedent
8
- from typing import Optional
9
-
7
+ import os
10
8
  from fastmcp import Context
11
- from loguru import logger
12
9
 
13
10
  from basic_memory.mcp.async_client import client
14
- from basic_memory.mcp.project_session import session, add_project_metadata
15
11
  from basic_memory.mcp.server import mcp
16
- from basic_memory.mcp.tools.utils import call_get, call_put, call_post, call_delete
17
- from basic_memory.schemas import ProjectInfoResponse
18
- from basic_memory.schemas.project_info import ProjectList, ProjectStatusResponse, ProjectInfoRequest
12
+ from basic_memory.mcp.tools.utils import call_get, call_post, call_delete
13
+ from basic_memory.schemas.project_info import (
14
+ ProjectList,
15
+ ProjectStatusResponse,
16
+ ProjectInfoRequest,
17
+ )
19
18
  from basic_memory.utils import generate_permalink
20
19
 
21
20
 
22
21
  @mcp.tool("list_memory_projects")
23
- async def list_memory_projects(
24
- ctx: Context | None = None, _compatibility: Optional[str] = None
25
- ) -> str:
22
+ async def list_memory_projects(context: Context | None = None) -> str:
26
23
  """List all available projects with their status.
27
24
 
28
- Shows all Basic Memory projects that are available, indicating which one
29
- is currently active and which is the default.
25
+ Shows all Basic Memory projects that are available for MCP operations.
26
+ Use this tool to discover projects when you need to know which project to use.
27
+
28
+ Use this tool:
29
+ - At conversation start when project is unknown
30
+ - When user asks about available projects
31
+ - Before any operation requiring a project
32
+
33
+ After calling:
34
+ - Ask user which project to use
35
+ - Remember their choice for the session
30
36
 
31
37
  Returns:
32
- Formatted list of projects with status indicators
38
+ Formatted list of projects with session management guidance
33
39
 
34
40
  Example:
35
41
  list_memory_projects()
36
42
  """
37
- if ctx: # pragma: no cover
38
- await ctx.info("Listing all available projects")
43
+ if context: # pragma: no cover
44
+ await context.info("Listing all available projects")
45
+
46
+ # Check if server is constrained to a specific project
47
+ constrained_project = os.environ.get("BASIC_MEMORY_MCP_PROJECT")
39
48
 
40
49
  # Get projects from API
41
50
  response = await call_get(client, "/projects/projects")
42
51
  project_list = ProjectList.model_validate(response.json())
43
52
 
44
- current = session.get_current_project()
45
-
46
- result = "Available projects:\n"
53
+ if constrained_project:
54
+ result = f"Project: {constrained_project}\n\n"
55
+ result += "Note: This MCP server is constrained to a single project.\n"
56
+ result += "All operations will automatically use this project."
57
+ else:
58
+ # Show all projects with session guidance
59
+ result = "Available projects:\n"
47
60
 
48
- for project in project_list.projects:
49
- indicators = []
50
- if project.name == current:
51
- indicators.append("current")
52
- if project.is_default:
53
- indicators.append("default")
54
-
55
- if indicators:
56
- result += f"• {project.name} ({', '.join(indicators)})\n"
57
- else:
61
+ for project in project_list.projects:
58
62
  result += f"• {project.name}\n"
59
63
 
60
- return add_project_metadata(result, current)
61
-
62
-
63
- @mcp.tool()
64
- async def switch_project(project_name: str, ctx: Context | None = None) -> str:
65
- """Switch to a different project context.
66
-
67
- Changes the active project context for all subsequent tool calls.
68
- Shows a project summary after switching successfully.
69
-
70
- Args:
71
- project_name: Name of the project to switch to
72
-
73
- Returns:
74
- Confirmation message with project summary
75
-
76
- Example:
77
- switch_project("work-notes")
78
- switch_project("personal-journal")
79
- """
80
- if ctx: # pragma: no cover
81
- await ctx.info(f"Switching to project: {project_name}")
82
-
83
- project_permalink = generate_permalink(project_name)
84
- current_project = session.get_current_project()
85
- try:
86
- # Validate project exists by getting project list
87
- response = await call_get(client, "/projects/projects")
88
- project_list = ProjectList.model_validate(response.json())
89
-
90
- # Find the project by name (case-insensitive) or permalink
91
- target_project = None
92
- for p in project_list.projects:
93
- # Match by permalink (handles case-insensitive input)
94
- if p.permalink == project_permalink:
95
- target_project = p
96
- break
97
- # Also match by name comparison (case-insensitive)
98
- if p.name.lower() == project_name.lower():
99
- target_project = p
100
- break
101
-
102
- if not target_project:
103
- available_projects = [p.name for p in project_list.projects]
104
- return f"Error: Project '{project_name}' not found. Available projects: {', '.join(available_projects)}"
105
-
106
- # Switch to the project using the canonical name from database
107
- canonical_name = target_project.name
108
- session.set_current_project(canonical_name)
109
- current_project = session.get_current_project()
110
-
111
- # Get project info to show summary
112
- try:
113
- current_project_permalink = generate_permalink(canonical_name)
114
- response = await call_get(
115
- client,
116
- f"/{current_project_permalink}/project/info",
117
- params={"project_name": canonical_name},
118
- )
119
- project_info = ProjectInfoResponse.model_validate(response.json())
120
-
121
- result = f"✓ Switched to {canonical_name} project\n\n"
122
- result += "Project Summary:\n"
123
- result += f"• {project_info.statistics.total_entities} entities\n"
124
- result += f"• {project_info.statistics.total_observations} observations\n"
125
- result += f"• {project_info.statistics.total_relations} relations\n"
126
-
127
- except Exception as e:
128
- # If we can't get project info, still confirm the switch
129
- logger.warning(f"Could not get project info for {canonical_name}: {e}")
130
- result = f"✓ Switched to {canonical_name} project\n\n"
131
- result += "Project summary unavailable.\n"
132
-
133
- return add_project_metadata(result, canonical_name)
134
-
135
- except Exception as e:
136
- logger.error(f"Error switching to project {project_name}: {e}")
137
- # Revert to previous project on error
138
- session.set_current_project(current_project)
139
-
140
- # Return user-friendly error message instead of raising exception
141
- return dedent(f"""
142
- # Project Switch Failed
143
-
144
- Could not switch to project '{project_name}': {str(e)}
145
-
146
- ## Current project: {current_project}
147
- Your session remains on the previous project.
148
-
149
- ## Troubleshooting:
150
- 1. **Check available projects**: Use `list_memory_projects()` to see valid project names
151
- 2. **Verify spelling**: Ensure the project name is spelled correctly
152
- 3. **Check permissions**: Verify you have access to the requested project
153
- 4. **Try again**: The error might be temporary
154
-
155
- ## Available options:
156
- - See all projects: `list_memory_projects()`
157
- - Stay on current project: `get_current_project()`
158
- - Try different project: `switch_project("correct-project-name")`
159
-
160
- If the project should exist but isn't listed, send a message to support@basicmachines.co.
161
- """).strip()
162
-
64
+ result += "\n" + "─" * 40 + "\n"
65
+ result += "Next: Ask which project to use for this session.\n"
66
+ result += "Example: 'Which project should I use for this task?'\n\n"
67
+ result += "Session reminder: Track the selected project for all subsequent operations in this conversation.\n"
68
+ result += "The user can say 'switch to [project]' to change projects."
163
69
 
164
- @mcp.tool()
165
- async def get_current_project(
166
- ctx: Context | None = None, _compatibility: Optional[str] = None
167
- ) -> str:
168
- """Show the currently active project and basic stats.
169
-
170
- Displays which project is currently active and provides basic information
171
- about it.
172
-
173
- Returns:
174
- Current project name and basic statistics
175
-
176
- Example:
177
- get_current_project()
178
- """
179
- if ctx: # pragma: no cover
180
- await ctx.info("Getting current project information")
181
-
182
- current_project = session.get_current_project()
183
- result = f"Current project: {current_project}\n\n"
184
-
185
- # get project stats (use permalink in URL path)
186
- current_project_permalink = generate_permalink(current_project)
187
- response = await call_get(
188
- client,
189
- f"/{current_project_permalink}/project/info",
190
- params={"project_name": current_project},
191
- )
192
- project_info = ProjectInfoResponse.model_validate(response.json())
193
-
194
- result += f"• {project_info.statistics.total_entities} entities\n"
195
- result += f"• {project_info.statistics.total_observations} observations\n"
196
- result += f"• {project_info.statistics.total_relations} relations\n"
197
-
198
- default_project = session.get_default_project()
199
- if current_project != default_project:
200
- result += f"• Default project: {default_project}\n"
201
-
202
- return add_project_metadata(result, current_project)
203
-
204
-
205
- @mcp.tool()
206
- async def set_default_project(project_name: str, ctx: Context | None = None) -> str:
207
- """Set default project in config. Requires restart to take effect.
208
-
209
- Updates the configuration to use a different default project. This change
210
- only takes effect after restarting the Basic Memory server.
211
-
212
- Args:
213
- project_name: Name of the project to set as default
214
-
215
- Returns:
216
- Confirmation message about config update
217
-
218
- Example:
219
- set_default_project("work-notes")
220
- """
221
- if ctx: # pragma: no cover
222
- await ctx.info(f"Setting default project to: {project_name}")
223
-
224
- # Call API to set default project using URL encoding for special characters
225
- from urllib.parse import quote
226
- encoded_name = quote(project_name, safe='')
227
- response = await call_put(client, f"/projects/{encoded_name}/default")
228
- status_response = ProjectStatusResponse.model_validate(response.json())
229
-
230
- result = f"✓ {status_response.message}\n\n"
231
- result += "Restart Basic Memory for this change to take effect:\n"
232
- result += "basic-memory mcp\n"
233
-
234
- if status_response.old_project:
235
- result += f"\nPrevious default: {status_response.old_project.name}\n"
236
-
237
- return add_project_metadata(result, session.get_current_project())
70
+ return result
238
71
 
239
72
 
240
73
  @mcp.tool("create_memory_project")
241
74
  async def create_memory_project(
242
- project_name: str, project_path: str, set_default: bool = False, ctx: Context | None = None
75
+ project_name: str, project_path: str, set_default: bool = False, context: Context | None = None
243
76
  ) -> str:
244
77
  """Create a new Basic Memory project.
245
78
 
@@ -258,8 +91,13 @@ async def create_memory_project(
258
91
  create_memory_project("my-research", "~/Documents/research")
259
92
  create_memory_project("work-notes", "/home/user/work", set_default=True)
260
93
  """
261
- if ctx: # pragma: no cover
262
- await ctx.info(f"Creating project: {project_name} at {project_path}")
94
+ # Check if server is constrained to a specific project
95
+ constrained_project = os.environ.get("BASIC_MEMORY_MCP_PROJECT")
96
+ if constrained_project:
97
+ return f'# Error\n\nProject creation disabled - MCP server is constrained to project \'{constrained_project}\'.\nUse the CLI to create projects: `basic-memory project add "{project_name}" "{project_path}"`'
98
+
99
+ if context: # pragma: no cover
100
+ await context.info(f"Creating project: {project_name} at {project_path}")
263
101
 
264
102
  # Create the project request
265
103
  project_request = ProjectInfoRequest(
@@ -280,17 +118,14 @@ async def create_memory_project(
280
118
  if set_default:
281
119
  result += "• Set as default project\n"
282
120
 
283
- result += "\nProject is now available for use.\n"
284
-
285
- # If project was set as default, update session
286
- if set_default:
287
- session.set_current_project(project_name)
121
+ result += "\nProject is now available for use in tool calls.\n"
122
+ result += f"Use '{project_name}' as the project parameter in MCP tool calls.\n"
288
123
 
289
- return add_project_metadata(result, session.get_current_project())
124
+ return result
290
125
 
291
126
 
292
127
  @mcp.tool()
293
- async def delete_project(project_name: str, ctx: Context | None = None) -> str:
128
+ async def delete_project(project_name: str, context: Context | None = None) -> str:
294
129
  """Delete a Basic Memory project.
295
130
 
296
131
  Removes a project from the configuration and database. This does NOT delete
@@ -310,16 +145,13 @@ async def delete_project(project_name: str, ctx: Context | None = None) -> str:
310
145
  This action cannot be undone. The project will need to be re-added
311
146
  to access its content through Basic Memory again.
312
147
  """
313
- if ctx: # pragma: no cover
314
- await ctx.info(f"Deleting project: {project_name}")
148
+ # Check if server is constrained to a specific project
149
+ constrained_project = os.environ.get("BASIC_MEMORY_MCP_PROJECT")
150
+ if constrained_project:
151
+ return f"# Error\n\nProject deletion disabled - MCP server is constrained to project '{constrained_project}'.\nUse the CLI to delete projects: `basic-memory project remove \"{project_name}\"`"
315
152
 
316
- current_project = session.get_current_project()
317
-
318
- # Check if trying to delete current project
319
- if project_name == current_project:
320
- raise ValueError(
321
- f"Cannot delete the currently active project '{project_name}'. Switch to a different project first."
322
- )
153
+ if context: # pragma: no cover
154
+ await context.info(f"Deleting project: {project_name}")
323
155
 
324
156
  # Get project info before deletion to validate it exists
325
157
  response = await call_get(client, "/projects/projects")
@@ -337,7 +169,7 @@ async def delete_project(project_name: str, ctx: Context | None = None) -> str:
337
169
  if p.name.lower() == project_name.lower():
338
170
  target_project = p
339
171
  break
340
-
172
+
341
173
  if not target_project:
342
174
  available_projects = [p.name for p in project_list.projects]
343
175
  raise ValueError(
@@ -346,7 +178,8 @@ async def delete_project(project_name: str, ctx: Context | None = None) -> str:
346
178
 
347
179
  # Call API to delete project using URL encoding for special characters
348
180
  from urllib.parse import quote
349
- encoded_name = quote(target_project.name, safe='')
181
+
182
+ encoded_name = quote(target_project.name, safe="")
350
183
  response = await call_delete(client, f"/projects/{encoded_name}")
351
184
  status_response = ProjectStatusResponse.model_validate(response.json())
352
185
 
@@ -361,4 +194,4 @@ async def delete_project(project_name: str, ctx: Context | None = None) -> str:
361
194
  result += "Files remain on disk but project is no longer tracked by Basic Memory.\n"
362
195
  result += "Re-add the project to access its content again.\n"
363
196
 
364
- return add_project_metadata(result, session.get_current_project())
197
+ return result
@@ -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
8
  import base64
10
9
  import io
11
10
 
11
+ from typing import Optional
12
+
12
13
  from loguru import logger
13
14
  from PIL import Image as PILImage
15
+ from fastmcp import Context
14
16
 
17
+ from basic_memory.mcp.project_context import get_active_project
15
18
  from basic_memory.mcp.server import mcp
16
19
  from basic_memory.mcp.async_client import client
17
20
  from basic_memory.mcp.tools.utils import call_get
18
- from basic_memory.mcp.project_session import get_active_project
19
21
  from basic_memory.schemas.memory import memory_url_path
20
22
  from basic_memory.utils import validate_project_path
21
23
 
@@ -147,11 +149,16 @@ def optimize_image(img, content_length, max_output_bytes=350000):
147
149
 
148
150
 
149
151
  @mcp.tool(description="Read a file's raw content by path or permalink")
150
- async def read_content(path: str, project: Optional[str] = None) -> dict:
152
+ async def read_content(
153
+ path: str, project: Optional[str] = None, context: Context | None = None
154
+ ) -> dict:
151
155
  """Read a file's raw content by path or permalink.
152
156
 
153
157
  This tool provides direct access to file content in the knowledge base,
154
- handling different file types appropriately:
158
+ handling different file types appropriately. Uses stateless architecture -
159
+ project parameter optional with server resolution.
160
+
161
+ Supported file types:
155
162
  - Text files (markdown, code, etc.) are returned as plain text
156
163
  - Images are automatically resized/optimized for display
157
164
  - Other binary files are returned as base64 if below size limits
@@ -161,7 +168,9 @@ async def read_content(path: str, project: Optional[str] = None) -> dict:
161
168
  - A regular file path (docs/example.md)
162
169
  - A memory URL (memory://docs/example)
163
170
  - A permalink (docs/example)
164
- project: Optional project name to read from. If not provided, uses current active project.
171
+ project: Project name to read from. Optional - server will resolve using hierarchy.
172
+ If unknown, use list_memory_projects() to discover available projects.
173
+ context: Optional FastMCP context for performance caching.
165
174
 
166
175
  Returns:
167
176
  A dictionary with the file content and metadata:
@@ -172,20 +181,27 @@ async def read_content(path: str, project: Optional[str] = None) -> dict:
172
181
 
173
182
  Examples:
174
183
  # Read a markdown file
175
- result = await read_file("docs/project-specs.md")
184
+ result = await read_content("docs/project-specs.md")
176
185
 
177
186
  # Read an image
178
- image_data = await read_file("assets/diagram.png")
187
+ image_data = await read_content("assets/diagram.png")
179
188
 
180
189
  # Read using memory URL
181
- content = await read_file("memory://docs/architecture")
190
+ content = await read_content("memory://docs/architecture")
191
+
192
+ # Read configuration file
193
+ config = await read_content("config/settings.json")
194
+
195
+ # Explicit project specification
196
+ result = await read_content("docs/project-specs.md", project="my-project")
182
197
 
183
- # Read from specific project
184
- content = await read_content("docs/example.md", project="work-project")
198
+ Raises:
199
+ HTTPError: If project doesn't exist or is inaccessible
200
+ SecurityError: If path attempts path traversal
185
201
  """
186
- logger.info("Reading file", path=path)
202
+ logger.info("Reading file", path=path, project=project)
187
203
 
188
- active_project = get_active_project(project)
204
+ active_project = await get_active_project(client, project, context)
189
205
  project_url = active_project.project_url
190
206
 
191
207
  url = memory_url_path(path)