basic-memory 0.14.3__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 (90) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  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/api/routers/resource_router.py +3 -3
  7. basic_memory/cli/app.py +9 -28
  8. basic_memory/cli/auth.py +277 -0
  9. basic_memory/cli/commands/cloud/__init__.py +5 -0
  10. basic_memory/cli/commands/cloud/api_client.py +112 -0
  11. basic_memory/cli/commands/cloud/bisync_commands.py +818 -0
  12. basic_memory/cli/commands/cloud/core_commands.py +288 -0
  13. basic_memory/cli/commands/cloud/mount_commands.py +295 -0
  14. basic_memory/cli/commands/cloud/rclone_config.py +288 -0
  15. basic_memory/cli/commands/cloud/rclone_installer.py +198 -0
  16. basic_memory/cli/commands/command_utils.py +60 -0
  17. basic_memory/cli/commands/import_memory_json.py +0 -4
  18. basic_memory/cli/commands/mcp.py +16 -4
  19. basic_memory/cli/commands/project.py +141 -145
  20. basic_memory/cli/commands/status.py +34 -22
  21. basic_memory/cli/commands/sync.py +45 -228
  22. basic_memory/cli/commands/tool.py +87 -16
  23. basic_memory/cli/main.py +1 -0
  24. basic_memory/config.py +96 -20
  25. basic_memory/db.py +104 -3
  26. basic_memory/deps.py +20 -3
  27. basic_memory/file_utils.py +89 -0
  28. basic_memory/ignore_utils.py +295 -0
  29. basic_memory/importers/chatgpt_importer.py +1 -1
  30. basic_memory/importers/utils.py +2 -2
  31. basic_memory/markdown/entity_parser.py +2 -2
  32. basic_memory/markdown/markdown_processor.py +2 -2
  33. basic_memory/markdown/plugins.py +39 -21
  34. basic_memory/markdown/utils.py +1 -1
  35. basic_memory/mcp/async_client.py +22 -10
  36. basic_memory/mcp/project_context.py +141 -0
  37. basic_memory/mcp/prompts/ai_assistant_guide.py +49 -4
  38. basic_memory/mcp/prompts/continue_conversation.py +1 -1
  39. basic_memory/mcp/prompts/recent_activity.py +116 -32
  40. basic_memory/mcp/prompts/search.py +1 -1
  41. basic_memory/mcp/prompts/utils.py +11 -4
  42. basic_memory/mcp/resources/ai_assistant_guide.md +179 -41
  43. basic_memory/mcp/resources/project_info.py +20 -6
  44. basic_memory/mcp/server.py +0 -37
  45. basic_memory/mcp/tools/__init__.py +5 -6
  46. basic_memory/mcp/tools/build_context.py +39 -19
  47. basic_memory/mcp/tools/canvas.py +19 -8
  48. basic_memory/mcp/tools/chatgpt_tools.py +178 -0
  49. basic_memory/mcp/tools/delete_note.py +67 -34
  50. basic_memory/mcp/tools/edit_note.py +55 -39
  51. basic_memory/mcp/tools/headers.py +44 -0
  52. basic_memory/mcp/tools/list_directory.py +18 -8
  53. basic_memory/mcp/tools/move_note.py +119 -41
  54. basic_memory/mcp/tools/project_management.py +77 -229
  55. basic_memory/mcp/tools/read_content.py +28 -12
  56. basic_memory/mcp/tools/read_note.py +97 -57
  57. basic_memory/mcp/tools/recent_activity.py +441 -42
  58. basic_memory/mcp/tools/search.py +82 -70
  59. basic_memory/mcp/tools/sync_status.py +5 -4
  60. basic_memory/mcp/tools/utils.py +19 -0
  61. basic_memory/mcp/tools/view_note.py +31 -6
  62. basic_memory/mcp/tools/write_note.py +65 -14
  63. basic_memory/models/knowledge.py +19 -2
  64. basic_memory/models/project.py +6 -2
  65. basic_memory/repository/entity_repository.py +31 -84
  66. basic_memory/repository/project_repository.py +1 -1
  67. basic_memory/repository/relation_repository.py +13 -0
  68. basic_memory/repository/repository.py +2 -2
  69. basic_memory/repository/search_repository.py +9 -3
  70. basic_memory/schemas/__init__.py +6 -0
  71. basic_memory/schemas/base.py +70 -12
  72. basic_memory/schemas/cloud.py +46 -0
  73. basic_memory/schemas/memory.py +99 -18
  74. basic_memory/schemas/project_info.py +9 -10
  75. basic_memory/schemas/sync_report.py +48 -0
  76. basic_memory/services/context_service.py +35 -11
  77. basic_memory/services/directory_service.py +7 -0
  78. basic_memory/services/entity_service.py +82 -52
  79. basic_memory/services/initialization.py +30 -11
  80. basic_memory/services/project_service.py +23 -33
  81. basic_memory/sync/sync_service.py +148 -24
  82. basic_memory/sync/watch_service.py +128 -44
  83. basic_memory/utils.py +181 -109
  84. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/METADATA +26 -96
  85. basic_memory-0.15.0.dist-info/RECORD +147 -0
  86. basic_memory/mcp/project_session.py +0 -120
  87. basic_memory-0.14.3.dist-info/RECORD +0 -132
  88. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/WHEEL +0 -0
  89. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/entry_points.txt +0 -0
  90. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/licenses/LICENSE +0 -0
@@ -4,240 +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
-
163
-
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"
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."
201
69
 
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
225
- response = await call_put(client, f"/projects/{project_name}/default")
226
- status_response = ProjectStatusResponse.model_validate(response.json())
227
-
228
- result = f"✓ {status_response.message}\n\n"
229
- result += "Restart Basic Memory for this change to take effect:\n"
230
- result += "basic-memory mcp\n"
231
-
232
- if status_response.old_project:
233
- result += f"\nPrevious default: {status_response.old_project.name}\n"
234
-
235
- return add_project_metadata(result, session.get_current_project())
70
+ return result
236
71
 
237
72
 
238
73
  @mcp.tool("create_memory_project")
239
74
  async def create_memory_project(
240
- 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
241
76
  ) -> str:
242
77
  """Create a new Basic Memory project.
243
78
 
@@ -256,8 +91,13 @@ async def create_memory_project(
256
91
  create_memory_project("my-research", "~/Documents/research")
257
92
  create_memory_project("work-notes", "/home/user/work", set_default=True)
258
93
  """
259
- if ctx: # pragma: no cover
260
- 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}")
261
101
 
262
102
  # Create the project request
263
103
  project_request = ProjectInfoRequest(
@@ -278,17 +118,14 @@ async def create_memory_project(
278
118
  if set_default:
279
119
  result += "• Set as default project\n"
280
120
 
281
- result += "\nProject is now available for use.\n"
282
-
283
- # If project was set as default, update session
284
- if set_default:
285
- 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"
286
123
 
287
- return add_project_metadata(result, session.get_current_project())
124
+ return result
288
125
 
289
126
 
290
127
  @mcp.tool()
291
- 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:
292
129
  """Delete a Basic Memory project.
293
130
 
294
131
  Removes a project from the configuration and database. This does NOT delete
@@ -308,31 +145,42 @@ async def delete_project(project_name: str, ctx: Context | None = None) -> str:
308
145
  This action cannot be undone. The project will need to be re-added
309
146
  to access its content through Basic Memory again.
310
147
  """
311
- if ctx: # pragma: no cover
312
- 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}\"`"
313
152
 
314
- current_project = session.get_current_project()
315
-
316
- # Check if trying to delete current project
317
- if project_name == current_project:
318
- raise ValueError(
319
- f"Cannot delete the currently active project '{project_name}'. Switch to a different project first."
320
- )
153
+ if context: # pragma: no cover
154
+ await context.info(f"Deleting project: {project_name}")
321
155
 
322
156
  # Get project info before deletion to validate it exists
323
157
  response = await call_get(client, "/projects/projects")
324
158
  project_list = ProjectList.model_validate(response.json())
325
159
 
326
- # Check if project exists
327
- project_exists = any(p.name == project_name for p in project_list.projects)
328
- if not project_exists:
160
+ # Find the project by name (case-insensitive) or permalink - same logic as switch_project
161
+ project_permalink = generate_permalink(project_name)
162
+ target_project = None
163
+ for p in project_list.projects:
164
+ # Match by permalink (handles case-insensitive input)
165
+ if p.permalink == project_permalink:
166
+ target_project = p
167
+ break
168
+ # Also match by name comparison (case-insensitive)
169
+ if p.name.lower() == project_name.lower():
170
+ target_project = p
171
+ break
172
+
173
+ if not target_project:
329
174
  available_projects = [p.name for p in project_list.projects]
330
175
  raise ValueError(
331
176
  f"Project '{project_name}' not found. Available projects: {', '.join(available_projects)}"
332
177
  )
333
178
 
334
- # Call API to delete project
335
- response = await call_delete(client, f"/projects/{project_name}")
179
+ # Call API to delete project using URL encoding for special characters
180
+ from urllib.parse import quote
181
+
182
+ encoded_name = quote(target_project.name, safe="")
183
+ response = await call_delete(client, f"/projects/{encoded_name}")
336
184
  status_response = ProjectStatusResponse.model_validate(response.json())
337
185
 
338
186
  result = f"✓ {status_response.message}\n\n"
@@ -346,4 +194,4 @@ async def delete_project(project_name: str, ctx: Context | None = None) -> str:
346
194
  result += "Files remain on disk but project is no longer tracked by Basic Memory.\n"
347
195
  result += "Re-add the project to access its content again.\n"
348
196
 
349
- 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)