basic-memory 0.14.4__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 (84) 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/directory_router.py +23 -2
  5. basic_memory/api/routers/knowledge_router.py +25 -8
  6. basic_memory/api/routers/project_router.py +100 -4
  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 +43 -0
  17. basic_memory/cli/commands/import_memory_json.py +0 -4
  18. basic_memory/cli/commands/mcp.py +77 -60
  19. basic_memory/cli/commands/project.py +154 -152
  20. basic_memory/cli/commands/status.py +25 -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 +131 -21
  25. basic_memory/db.py +104 -3
  26. basic_memory/deps.py +27 -8
  27. basic_memory/file_utils.py +37 -13
  28. basic_memory/ignore_utils.py +295 -0
  29. basic_memory/markdown/plugins.py +9 -7
  30. basic_memory/mcp/async_client.py +124 -14
  31. basic_memory/mcp/project_context.py +141 -0
  32. basic_memory/mcp/prompts/ai_assistant_guide.py +49 -4
  33. basic_memory/mcp/prompts/continue_conversation.py +17 -16
  34. basic_memory/mcp/prompts/recent_activity.py +116 -32
  35. basic_memory/mcp/prompts/search.py +13 -12
  36. basic_memory/mcp/prompts/utils.py +11 -4
  37. basic_memory/mcp/resources/ai_assistant_guide.md +211 -341
  38. basic_memory/mcp/resources/project_info.py +27 -11
  39. basic_memory/mcp/server.py +0 -37
  40. basic_memory/mcp/tools/__init__.py +5 -6
  41. basic_memory/mcp/tools/build_context.py +67 -56
  42. basic_memory/mcp/tools/canvas.py +38 -26
  43. basic_memory/mcp/tools/chatgpt_tools.py +187 -0
  44. basic_memory/mcp/tools/delete_note.py +81 -47
  45. basic_memory/mcp/tools/edit_note.py +155 -138
  46. basic_memory/mcp/tools/list_directory.py +112 -99
  47. basic_memory/mcp/tools/move_note.py +181 -101
  48. basic_memory/mcp/tools/project_management.py +113 -277
  49. basic_memory/mcp/tools/read_content.py +91 -74
  50. basic_memory/mcp/tools/read_note.py +152 -115
  51. basic_memory/mcp/tools/recent_activity.py +471 -68
  52. basic_memory/mcp/tools/search.py +105 -92
  53. basic_memory/mcp/tools/sync_status.py +136 -130
  54. basic_memory/mcp/tools/utils.py +4 -0
  55. basic_memory/mcp/tools/view_note.py +44 -33
  56. basic_memory/mcp/tools/write_note.py +151 -90
  57. basic_memory/models/knowledge.py +12 -6
  58. basic_memory/models/project.py +6 -2
  59. basic_memory/repository/entity_repository.py +89 -82
  60. basic_memory/repository/relation_repository.py +13 -0
  61. basic_memory/repository/repository.py +18 -5
  62. basic_memory/repository/search_repository.py +46 -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/directory_service.py +124 -3
  71. basic_memory/services/entity_service.py +100 -48
  72. basic_memory/services/initialization.py +30 -11
  73. basic_memory/services/project_service.py +101 -24
  74. basic_memory/services/search_service.py +16 -8
  75. basic_memory/sync/sync_service.py +173 -34
  76. basic_memory/sync/watch_service.py +101 -40
  77. basic_memory/utils.py +14 -4
  78. {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/METADATA +57 -9
  79. basic_memory-0.15.1.dist-info/RECORD +146 -0
  80. basic_memory/mcp/project_session.py +0 -120
  81. basic_memory-0.14.4.dist-info/RECORD +0 -133
  82. {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/WHEEL +0 -0
  83. {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/entry_points.txt +0 -0
  84. {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -3,10 +3,12 @@
3
3
  from typing import Optional
4
4
 
5
5
  from loguru import logger
6
+ from fastmcp import Context
6
7
 
7
8
  from basic_memory.config import ConfigManager
9
+ from basic_memory.mcp.async_client import get_client
8
10
  from basic_memory.mcp.server import mcp
9
- from basic_memory.mcp.project_session import get_active_project
11
+ from basic_memory.mcp.project_context import get_active_project
10
12
  from basic_memory.services.sync_status_service import sync_status_tracker
11
13
 
12
14
 
@@ -79,7 +81,7 @@ def _get_all_projects_status() -> list[str]:
79
81
  - Background processing of knowledge graphs
80
82
  """,
81
83
  )
82
- async def sync_status(project: Optional[str] = None) -> str:
84
+ async def sync_status(project: Optional[str] = None, context: Context | None = None) -> str:
83
85
  """Get current sync status and system readiness information.
84
86
 
85
87
  This tool provides detailed information about any ongoing or completed
@@ -93,163 +95,167 @@ async def sync_status(project: Optional[str] = None) -> str:
93
95
  """
94
96
  logger.info("MCP tool call tool=sync_status")
95
97
 
96
- status_lines = []
98
+ async with get_client() as client:
99
+ status_lines = []
97
100
 
98
- try:
99
- from basic_memory.services.sync_status_service import sync_status_tracker
100
-
101
- # Get overall summary
102
- summary = sync_status_tracker.get_summary()
103
- is_ready = sync_status_tracker.is_ready
104
-
105
- # Header
106
- status_lines.extend(
107
- [
108
- "# Basic Memory Sync Status",
109
- "",
110
- f"**Current Status**: {summary}",
111
- f"**System Ready**: {'✅ Yes' if is_ready else '🔄 Processing'}",
112
- "",
113
- ]
114
- )
115
-
116
- if is_ready:
101
+ try:
102
+ from basic_memory.services.sync_status_service import sync_status_tracker
103
+
104
+ # Get overall summary
105
+ summary = sync_status_tracker.get_summary()
106
+ is_ready = sync_status_tracker.is_ready
107
+
108
+ # Header
117
109
  status_lines.extend(
118
110
  [
119
- " **All sync operations completed**",
111
+ "# Basic Memory Sync Status",
120
112
  "",
121
- "- File indexing is complete",
122
- "- Knowledge graphs are up to date",
123
- "- All Basic Memory tools are fully operational",
113
+ f"**Current Status**: {summary}",
114
+ f"**System Ready**: {'✅ Yes' if is_ready else '🔄 Processing'}",
124
115
  "",
125
- "Your knowledge base is ready for use!",
126
116
  ]
127
117
  )
128
118
 
129
- # Show all projects status even when ready
130
- status_lines.extend(_get_all_projects_status())
131
- else:
132
- # System is still processing - show both active and all projects
133
- all_sync_projects = sync_status_tracker.get_all_projects()
134
-
135
- active_projects = [
136
- p for p in all_sync_projects.values() if p.status.value in ["scanning", "syncing"]
137
- ]
138
- failed_projects = [p for p in all_sync_projects.values() if p.status.value == "failed"]
139
-
140
- if active_projects:
119
+ if is_ready:
141
120
  status_lines.extend(
142
121
  [
143
- "🔄 **File synchronization in progress**",
122
+ " **All sync operations completed**",
144
123
  "",
145
- "Basic Memory is automatically processing all configured projects and building knowledge graphs.",
146
- "This typically takes 1-3 minutes depending on the amount of content.",
124
+ "- File indexing is complete",
125
+ "- Knowledge graphs are up to date",
126
+ "- All Basic Memory tools are fully operational",
147
127
  "",
148
- "**Currently Processing:**",
128
+ "Your knowledge base is ready for use!",
149
129
  ]
150
130
  )
151
131
 
152
- for project_status in active_projects:
153
- progress = ""
154
- if project_status.files_total > 0:
155
- progress_pct = (
156
- project_status.files_processed / project_status.files_total
157
- ) * 100
158
- progress = f" ({project_status.files_processed}/{project_status.files_total}, {progress_pct:.0f}%)"
132
+ # Show all projects status even when ready
133
+ status_lines.extend(_get_all_projects_status())
134
+ else:
135
+ # System is still processing - show both active and all projects
136
+ all_sync_projects = sync_status_tracker.get_all_projects()
159
137
 
160
- status_lines.append(
161
- f"- **{project_status.project_name}**: {project_status.message}{progress}"
162
- )
163
-
164
- status_lines.extend(
165
- [
166
- "",
167
- "**What's happening:**",
168
- "- Scanning and indexing markdown files",
169
- "- Building entity and relationship graphs",
170
- "- Setting up full-text search indexes",
171
- "- Processing file changes and updates",
172
- "",
173
- "**What you can do:**",
174
- "- Wait for automatic processing to complete - no action needed",
175
- "- Use this tool again to check progress",
176
- "- Simple operations may work already",
177
- "- All projects will be available once sync finishes",
178
- ]
179
- )
180
-
181
- # Handle failed projects (independent of active projects)
182
- if failed_projects:
183
- status_lines.extend(["", "❌ **Some projects failed to sync:**", ""])
138
+ active_projects = [
139
+ p
140
+ for p in all_sync_projects.values()
141
+ if p.status.value in ["scanning", "syncing"]
142
+ ]
143
+ failed_projects = [
144
+ p for p in all_sync_projects.values() if p.status.value == "failed"
145
+ ]
184
146
 
185
- for project_status in failed_projects:
186
- status_lines.append(
187
- f"- **{project_status.project_name}**: {project_status.error or 'Unknown error'}"
147
+ if active_projects:
148
+ status_lines.extend(
149
+ [
150
+ "🔄 **File synchronization in progress**",
151
+ "",
152
+ "Basic Memory is automatically processing all configured projects and building knowledge graphs.",
153
+ "This typically takes 1-3 minutes depending on the amount of content.",
154
+ "",
155
+ "**Currently Processing:**",
156
+ ]
188
157
  )
189
158
 
190
- status_lines.extend(
191
- [
192
- "",
193
- "**Next steps:**",
194
- "1. Check the logs for detailed error information",
195
- "2. Ensure file permissions allow read/write access",
196
- "3. Try restarting the MCP server",
197
- "4. If issues persist, consider filing a support issue",
198
- ]
199
- )
200
- elif not active_projects:
201
- # No active or failed projects - must be pending
202
- status_lines.extend(
203
- [
204
- "⏳ **Sync operations pending**",
205
- "",
206
- "File synchronization has been queued but hasn't started yet.",
207
- "This usually resolves automatically within a few seconds.",
208
- ]
209
- )
159
+ for project_status in active_projects:
160
+ progress = ""
161
+ if project_status.files_total > 0:
162
+ progress_pct = (
163
+ project_status.files_processed / project_status.files_total
164
+ ) * 100
165
+ progress = f" ({project_status.files_processed}/{project_status.files_total}, {progress_pct:.0f}%)"
166
+
167
+ status_lines.append(
168
+ f"- **{project_status.project_name}**: {project_status.message}{progress}"
169
+ )
170
+
171
+ status_lines.extend(
172
+ [
173
+ "",
174
+ "**What's happening:**",
175
+ "- Scanning and indexing markdown files",
176
+ "- Building entity and relationship graphs",
177
+ "- Settings up full-text search indexes",
178
+ "- Processing file changes and updates",
179
+ "",
180
+ "**What you can do:**",
181
+ "- Wait for automatic processing to complete - no action needed",
182
+ "- Use this tool again to check progress",
183
+ "- Simple operations may work already",
184
+ "- All projects will be available once sync finishes",
185
+ ]
186
+ )
210
187
 
211
- # Add comprehensive project status for all configured projects
212
- all_projects_status = _get_all_projects_status()
213
- if all_projects_status:
214
- status_lines.extend(all_projects_status)
188
+ # Handle failed projects (independent of active projects)
189
+ if failed_projects:
190
+ status_lines.extend(["", "❌ **Some projects failed to sync:**", ""])
191
+
192
+ for project_status in failed_projects:
193
+ status_lines.append(
194
+ f"- **{project_status.project_name}**: {project_status.error or 'Unknown error'}"
195
+ )
196
+
197
+ status_lines.extend(
198
+ [
199
+ "",
200
+ "**Next steps:**",
201
+ "1. Check the logs for detailed error information",
202
+ "2. Ensure file permissions allow read/write access",
203
+ "3. Try restarting the MCP server",
204
+ "4. If issues persist, consider filing a support issue",
205
+ ]
206
+ )
207
+ elif not active_projects:
208
+ # No active or failed projects - must be pending
209
+ status_lines.extend(
210
+ [
211
+ "⏳ **Sync operations pending**",
212
+ "",
213
+ "File synchronization has been queued but hasn't started yet.",
214
+ "This usually resolves automatically within a few seconds.",
215
+ ]
216
+ )
215
217
 
216
- # Add explanation about automatic syncing if there are unsynced projects
217
- unsynced_count = sum(1 for line in all_projects_status if "⏳" in line)
218
- if unsynced_count > 0 and not is_ready:
219
- status_lines.extend(
220
- [
221
- "",
222
- "**Note**: All configured projects will be automatically synced during startup.",
223
- "You don't need to manually switch projects - Basic Memory handles this for you.",
224
- ]
225
- )
218
+ # Add comprehensive project status for all configured projects
219
+ all_projects_status = _get_all_projects_status()
220
+ if all_projects_status:
221
+ status_lines.extend(all_projects_status)
222
+
223
+ # Add explanation about automatic syncing if there are unsynced projects
224
+ unsynced_count = sum(1 for line in all_projects_status if "⏳" in line)
225
+ if unsynced_count > 0 and not is_ready:
226
+ status_lines.extend(
227
+ [
228
+ "",
229
+ "**Note**: All configured projects will be automatically synced during startup.",
230
+ ]
231
+ )
226
232
 
227
- # Add project context if provided
228
- if project:
229
- try:
230
- active_project = get_active_project(project)
231
- status_lines.extend(
232
- [
233
- "",
234
- "---",
235
- "",
236
- f"**Active Project**: {active_project.name}",
237
- f"**Project Path**: {active_project.home}",
238
- ]
239
- )
240
- except Exception as e:
241
- logger.debug(f"Could not get project info: {e}")
233
+ # Add project context if provided
234
+ if project:
235
+ try:
236
+ active_project = await get_active_project(client, project, context)
237
+ status_lines.extend(
238
+ [
239
+ "",
240
+ "---",
241
+ "",
242
+ f"**Active Project**: {active_project.name}",
243
+ f"**Project Path**: {active_project.home}",
244
+ ]
245
+ )
246
+ except Exception as e:
247
+ logger.debug(f"Could not get project info: {e}")
242
248
 
243
- return "\n".join(status_lines)
249
+ return "\n".join(status_lines)
244
250
 
245
- except Exception as e:
246
- return f"""# Sync Status - Error
251
+ except Exception as e:
252
+ return f"""# Sync Status - Error
247
253
 
248
254
  ❌ **Unable to check sync status**: {str(e)}
249
255
 
250
256
  **Troubleshooting:**
251
257
  - The system may still be starting up
252
- - Try waiting a few seconds and checking again
258
+ - Try waiting a few seconds and checking again
253
259
  - Check logs for detailed error information
254
260
  - Consider restarting if the issue persists
255
261
  """
@@ -107,6 +107,7 @@ async def call_get(
107
107
  """
108
108
  logger.debug(f"Calling GET '{url}' params: '{params}'")
109
109
  error_message = None
110
+
110
111
  try:
111
112
  response = await client.get(
112
113
  url,
@@ -280,6 +281,7 @@ async def call_patch(
280
281
  ToolError: If the request fails with an appropriate error message
281
282
  """
282
283
  logger.debug(f"Calling PATCH '{url}'")
284
+
283
285
  try:
284
286
  response = await client.patch(
285
287
  url,
@@ -384,6 +386,7 @@ async def call_post(
384
386
  """
385
387
  logger.debug(f"Calling POST '{url}'")
386
388
  error_message = None
389
+
387
390
  try:
388
391
  response = await client.post(
389
392
  url=url,
@@ -465,6 +468,7 @@ async def call_delete(
465
468
  """
466
469
  logger.debug(f"Calling DELETE '{url}'")
467
470
  error_message = None
471
+
468
472
  try:
469
473
  response = await client.delete(
470
474
  url=url,
@@ -4,6 +4,7 @@ from textwrap import dedent
4
4
  from typing import Optional
5
5
 
6
6
  from loguru import logger
7
+ from fastmcp import Context
7
8
 
8
9
  from basic_memory.mcp.server import mcp
9
10
  from basic_memory.mcp.tools.read_note import read_note
@@ -13,54 +14,64 @@ from basic_memory.mcp.tools.read_note import read_note
13
14
  description="View a note as a formatted artifact for better readability.",
14
15
  )
15
16
  async def view_note(
16
- identifier: str, page: int = 1, page_size: int = 10, project: Optional[str] = None
17
+ identifier: str,
18
+ project: Optional[str] = None,
19
+ page: int = 1,
20
+ page_size: int = 10,
21
+ context: Context | None = None,
17
22
  ) -> str:
18
23
  """View a markdown note as a formatted artifact.
19
24
 
20
- This tool reads a note using the same logic as read_note but displays the content
21
- as a markdown artifact for better viewing experience in Claude Desktop.
22
-
23
- After calling this tool, create an artifact using the returned content to display
24
- the note in a readable format. The tool returns the note content that should be
25
- used to create a markdown artifact.
25
+ This tool reads a note using the same logic as read_note but instructs Claude
26
+ to display the content as a markdown artifact in the Claude Desktop app.
27
+ Project parameter optional with server resolution.
26
28
 
27
29
  Args:
28
30
  identifier: The title or permalink of the note to view
31
+ project: Project name to read from. Optional - server will resolve using hierarchy.
32
+ If unknown, use list_memory_projects() to discover available projects.
29
33
  page: Page number for paginated results (default: 1)
30
34
  page_size: Number of items per page (default: 10)
31
- project: Optional project name to read from. If not provided, uses current active project.
35
+ context: Optional FastMCP context for performance caching.
32
36
 
33
37
  Returns:
34
- The note content as a markdown artifact with a confirmation message.
38
+ Instructions for Claude to create a markdown artifact with the note content.
39
+
40
+ Examples:
41
+ # View a note by title
42
+ view_note("Meeting Notes")
43
+
44
+ # View a note by permalink
45
+ view_note("meetings/weekly-standup")
46
+
47
+ # View with pagination
48
+ view_note("large-document", page=2, page_size=5)
49
+
50
+ # Explicit project specification
51
+ view_note("Meeting Notes", project="my-project")
52
+
53
+ Raises:
54
+ HTTPError: If project doesn't exist or is inaccessible
55
+ SecurityError: If identifier attempts path traversal
35
56
  """
36
57
 
37
- logger.info(f"Viewing note: {identifier}")
58
+ logger.info(f"Viewing note: {identifier} in project: {project}")
38
59
 
39
60
  # Call the existing read_note logic
40
- content = await read_note.fn(identifier, page, page_size, project)
61
+ content = await read_note.fn(identifier, project, page, page_size, context)
41
62
 
42
63
  # Check if this is an error message (note not found)
43
- if "# Note Not Found:" in content:
44
- return content # Return error message directly instead of creating artifact
45
-
46
- # Extract title from content if possible
47
- title = identifier
48
- lines = content.split("\n")
49
- for line in lines:
50
- if line.startswith("title:"):
51
- # Extract title from frontmatter
52
- title = line.split("title:", 1)[1].strip().strip('"').strip("'")
53
- break
54
- elif line.startswith("# "):
55
- # Extract title from first heading
56
- title = line[2:].strip()
57
- break
58
-
59
- # Return the artifact XML as part of the response
60
- artifact = f'<artifact identifier="note-{hash(identifier) & 0x7FFFFFFF}" type="text/markdown" title="{title}">\n{content}\n</artifact>'
64
+ if "# Note Not Found" in content:
65
+ return content # Return error message directly
61
66
 
67
+ # Return instructions for Claude to create an artifact
62
68
  return dedent(f"""
63
- <instructions>
64
- Create an artifact using the returned artifact content to display the note in a readable format.
65
- </instructions>
66
- {artifact}\n\n✅ Note displayed as artifact: **{title}**""")
69
+ Note retrieved: "{identifier}"
70
+
71
+ Display this note as a markdown artifact for the user.
72
+
73
+ Content:
74
+ ---
75
+ {content}
76
+ ---
77
+ """).strip()