basic-memory 0.12.3__py3-none-any.whl → 0.13.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 (116) hide show
  1. basic_memory/__init__.py +2 -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/647e7a75e2cd_project_constraint_fix.py +104 -0
  5. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -6
  6. basic_memory/api/app.py +43 -13
  7. basic_memory/api/routers/__init__.py +4 -2
  8. basic_memory/api/routers/directory_router.py +63 -0
  9. basic_memory/api/routers/importer_router.py +152 -0
  10. basic_memory/api/routers/knowledge_router.py +139 -37
  11. basic_memory/api/routers/management_router.py +78 -0
  12. basic_memory/api/routers/memory_router.py +6 -62
  13. basic_memory/api/routers/project_router.py +234 -0
  14. basic_memory/api/routers/prompt_router.py +260 -0
  15. basic_memory/api/routers/search_router.py +3 -21
  16. basic_memory/api/routers/utils.py +130 -0
  17. basic_memory/api/template_loader.py +292 -0
  18. basic_memory/cli/app.py +20 -21
  19. basic_memory/cli/commands/__init__.py +2 -1
  20. basic_memory/cli/commands/auth.py +136 -0
  21. basic_memory/cli/commands/db.py +3 -3
  22. basic_memory/cli/commands/import_chatgpt.py +31 -207
  23. basic_memory/cli/commands/import_claude_conversations.py +16 -142
  24. basic_memory/cli/commands/import_claude_projects.py +33 -143
  25. basic_memory/cli/commands/import_memory_json.py +26 -83
  26. basic_memory/cli/commands/mcp.py +71 -18
  27. basic_memory/cli/commands/project.py +102 -70
  28. basic_memory/cli/commands/status.py +19 -9
  29. basic_memory/cli/commands/sync.py +44 -58
  30. basic_memory/cli/commands/tool.py +6 -6
  31. basic_memory/cli/main.py +1 -5
  32. basic_memory/config.py +143 -87
  33. basic_memory/db.py +6 -4
  34. basic_memory/deps.py +227 -30
  35. basic_memory/importers/__init__.py +27 -0
  36. basic_memory/importers/base.py +79 -0
  37. basic_memory/importers/chatgpt_importer.py +222 -0
  38. basic_memory/importers/claude_conversations_importer.py +172 -0
  39. basic_memory/importers/claude_projects_importer.py +148 -0
  40. basic_memory/importers/memory_json_importer.py +93 -0
  41. basic_memory/importers/utils.py +58 -0
  42. basic_memory/markdown/entity_parser.py +5 -2
  43. basic_memory/mcp/auth_provider.py +270 -0
  44. basic_memory/mcp/external_auth_provider.py +321 -0
  45. basic_memory/mcp/project_session.py +103 -0
  46. basic_memory/mcp/prompts/__init__.py +2 -0
  47. basic_memory/mcp/prompts/continue_conversation.py +18 -68
  48. basic_memory/mcp/prompts/recent_activity.py +20 -4
  49. basic_memory/mcp/prompts/search.py +14 -140
  50. basic_memory/mcp/prompts/sync_status.py +116 -0
  51. basic_memory/mcp/prompts/utils.py +3 -3
  52. basic_memory/mcp/{tools → resources}/project_info.py +6 -2
  53. basic_memory/mcp/server.py +86 -13
  54. basic_memory/mcp/supabase_auth_provider.py +463 -0
  55. basic_memory/mcp/tools/__init__.py +24 -0
  56. basic_memory/mcp/tools/build_context.py +43 -8
  57. basic_memory/mcp/tools/canvas.py +17 -3
  58. basic_memory/mcp/tools/delete_note.py +168 -5
  59. basic_memory/mcp/tools/edit_note.py +303 -0
  60. basic_memory/mcp/tools/list_directory.py +154 -0
  61. basic_memory/mcp/tools/move_note.py +299 -0
  62. basic_memory/mcp/tools/project_management.py +332 -0
  63. basic_memory/mcp/tools/read_content.py +15 -6
  64. basic_memory/mcp/tools/read_note.py +26 -7
  65. basic_memory/mcp/tools/recent_activity.py +11 -2
  66. basic_memory/mcp/tools/search.py +189 -8
  67. basic_memory/mcp/tools/sync_status.py +254 -0
  68. basic_memory/mcp/tools/utils.py +184 -12
  69. basic_memory/mcp/tools/view_note.py +66 -0
  70. basic_memory/mcp/tools/write_note.py +24 -17
  71. basic_memory/models/__init__.py +3 -2
  72. basic_memory/models/knowledge.py +16 -4
  73. basic_memory/models/project.py +78 -0
  74. basic_memory/models/search.py +8 -5
  75. basic_memory/repository/__init__.py +2 -0
  76. basic_memory/repository/entity_repository.py +8 -3
  77. basic_memory/repository/observation_repository.py +35 -3
  78. basic_memory/repository/project_info_repository.py +3 -2
  79. basic_memory/repository/project_repository.py +85 -0
  80. basic_memory/repository/relation_repository.py +8 -2
  81. basic_memory/repository/repository.py +107 -15
  82. basic_memory/repository/search_repository.py +192 -54
  83. basic_memory/schemas/__init__.py +6 -0
  84. basic_memory/schemas/base.py +33 -5
  85. basic_memory/schemas/directory.py +30 -0
  86. basic_memory/schemas/importer.py +34 -0
  87. basic_memory/schemas/memory.py +84 -13
  88. basic_memory/schemas/project_info.py +112 -2
  89. basic_memory/schemas/prompt.py +90 -0
  90. basic_memory/schemas/request.py +56 -2
  91. basic_memory/schemas/search.py +1 -1
  92. basic_memory/services/__init__.py +2 -1
  93. basic_memory/services/context_service.py +208 -95
  94. basic_memory/services/directory_service.py +167 -0
  95. basic_memory/services/entity_service.py +399 -6
  96. basic_memory/services/exceptions.py +6 -0
  97. basic_memory/services/file_service.py +14 -15
  98. basic_memory/services/initialization.py +170 -66
  99. basic_memory/services/link_resolver.py +35 -12
  100. basic_memory/services/migration_service.py +168 -0
  101. basic_memory/services/project_service.py +671 -0
  102. basic_memory/services/search_service.py +77 -2
  103. basic_memory/services/sync_status_service.py +181 -0
  104. basic_memory/sync/background_sync.py +25 -0
  105. basic_memory/sync/sync_service.py +102 -21
  106. basic_memory/sync/watch_service.py +63 -39
  107. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  108. basic_memory/templates/prompts/search.hbs +101 -0
  109. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0.dist-info}/METADATA +24 -2
  110. basic_memory-0.13.0.dist-info/RECORD +138 -0
  111. basic_memory/api/routers/project_info_router.py +0 -274
  112. basic_memory/mcp/main.py +0 -24
  113. basic_memory-0.12.3.dist-info/RECORD +0 -100
  114. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0.dist-info}/WHEEL +0 -0
  115. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0.dist-info}/entry_points.txt +0 -0
  116. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -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,24 @@ 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
+ # Check migration status and wait briefly if needed
56
+ from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
57
+
58
+ migration_status = await wait_for_migration_or_return_status(timeout=5.0)
59
+ if migration_status: # pragma: no cover
60
+ return f"# System Status\n\n{migration_status}\n\nPlease wait for migration to complete before reading notes."
61
+
62
+ active_project = get_active_project(project)
63
+ project_url = active_project.project_url
64
+
46
65
  # Get the file via REST API - first try direct permalink lookup
47
66
  entity_path = memory_url_path(identifier)
48
- path = f"/resource/{entity_path}"
67
+ path = f"{project_url}/resource/{entity_path}"
49
68
  logger.info(f"Attempting to read note from URL: {path}")
50
69
 
51
70
  try:
@@ -62,14 +81,14 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
62
81
 
63
82
  # Fallback 1: Try title search via API
64
83
  logger.info(f"Search title for: {identifier}")
65
- title_results = await search_notes(query=identifier, search_type="title")
84
+ title_results = await search_notes.fn(query=identifier, search_type="title", project=project)
66
85
 
67
86
  if title_results and title_results.results:
68
87
  result = title_results.results[0] # Get the first/best match
69
88
  if result.permalink:
70
89
  try:
71
90
  # Try to fetch the content using the found permalink
72
- path = f"/resource/{result.permalink}"
91
+ path = f"{project_url}/resource/{result.permalink}"
73
92
  response = await call_get(
74
93
  client, path, params={"page": page, "page_size": page_size}
75
94
  )
@@ -86,7 +105,7 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
86
105
 
87
106
  # Fallback 2: Text search as a last resort
88
107
  logger.info(f"Title search failed, trying text search for: {identifier}")
89
- text_results = await search_notes(query=identifier, search_type="text")
108
+ text_results = await search_notes.fn(query=identifier, search_type="text", project=project)
90
109
 
91
110
  # We didn't find a direct match, construct a helpful error message
92
111
  if not text_results or not text_results.results:
@@ -102,7 +121,7 @@ def format_not_found_message(identifier: str) -> str:
102
121
  return dedent(f"""
103
122
  # Note Not Found: "{identifier}"
104
123
 
105
- I couldn't find any notes matching "{identifier}". Here are some suggestions:
124
+ I searched for "{identifier}" using multiple methods (direct lookup, title search, and text search) but couldn't find any matching notes. Here are some suggestions:
106
125
 
107
126
  ## Check Identifier Type
108
127
  - If you provided a title, try using the exact permalink instead
@@ -148,7 +167,7 @@ def format_related_results(identifier: str, results) -> str:
148
167
  message = dedent(f"""
149
168
  # Note Not Found: "{identifier}"
150
169
 
151
- I couldn't find an exact match for "{identifier}", but I found some related notes:
170
+ I searched for "{identifier}" using direct lookup and title search but couldn't find an exact match. However, I found some related notes through text search:
152
171
 
153
172
  """)
154
173
 
@@ -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())
@@ -1,5 +1,6 @@
1
1
  """Search tools for Basic Memory MCP server."""
2
2
 
3
+ from textwrap import dedent
3
4
  from typing import List, Optional
4
5
 
5
6
  from loguru import logger
@@ -7,9 +8,166 @@ from loguru import logger
7
8
  from basic_memory.mcp.async_client import client
8
9
  from basic_memory.mcp.server import mcp
9
10
  from basic_memory.mcp.tools.utils import call_post
11
+ from basic_memory.mcp.project_session import get_active_project
10
12
  from basic_memory.schemas.search import SearchItemType, SearchQuery, SearchResponse
11
13
 
12
14
 
15
+ def _format_search_error_response(error_message: str, query: str, search_type: str = "text") -> str:
16
+ """Format helpful error responses for search failures that guide users to successful searches."""
17
+
18
+ # FTS5 syntax errors
19
+ if "syntax error" in error_message.lower() or "fts5" in error_message.lower():
20
+ clean_query = (
21
+ query.replace('"', "")
22
+ .replace("(", "")
23
+ .replace(")", "")
24
+ .replace("+", "")
25
+ .replace("*", "")
26
+ )
27
+ return dedent(f"""
28
+ # Search Failed - Invalid Syntax
29
+
30
+ The search query '{query}' contains invalid syntax that the search engine cannot process.
31
+
32
+ ## Common syntax issues:
33
+ 1. **Special characters**: Characters like `+`, `*`, `"`, `(`, `)` have special meaning in search
34
+ 2. **Unmatched quotes**: Make sure quotes are properly paired
35
+ 3. **Invalid operators**: Check AND, OR, NOT operators are used correctly
36
+
37
+ ## How to fix:
38
+ 1. **Simplify your search**: Try using simple words instead: `{clean_query}`
39
+ 2. **Remove special characters**: Use alphanumeric characters and spaces
40
+ 3. **Use basic boolean operators**: `word1 AND word2`, `word1 OR word2`, `word1 NOT word2`
41
+
42
+ ## Examples of valid searches:
43
+ - Simple text: `project planning`
44
+ - Boolean AND: `project AND planning`
45
+ - Boolean OR: `meeting OR discussion`
46
+ - Boolean NOT: `project NOT archived`
47
+ - Grouped: `(project OR planning) AND notes`
48
+
49
+ ## Try again with:
50
+ ```
51
+ search_notes("INSERT_CLEAN_QUERY_HERE")
52
+ ```
53
+
54
+ Replace INSERT_CLEAN_QUERY_HERE with your simplified search terms.
55
+ """).strip()
56
+
57
+ # Project not found errors (check before general "not found")
58
+ if "project not found" in error_message.lower():
59
+ return dedent(f"""
60
+ # Search Failed - Project Not Found
61
+
62
+ The current project is not accessible or doesn't exist: {error_message}
63
+
64
+ ## How to resolve:
65
+ 1. **Check available projects**: `list_projects()`
66
+ 2. **Switch to valid project**: `switch_project("valid-project-name")`
67
+ 3. **Verify project setup**: Ensure your project is properly configured
68
+
69
+ ## Current session info:
70
+ - Check current project: `get_current_project()`
71
+ - See available projects: `list_projects()`
72
+ """).strip()
73
+
74
+ # No results found
75
+ if "no results" in error_message.lower() or "not found" in error_message.lower():
76
+ simplified_query = (
77
+ " ".join(query.split()[:2])
78
+ if len(query.split()) > 2
79
+ else query.split()[0]
80
+ if query.split()
81
+ else "notes"
82
+ )
83
+ return dedent(f"""
84
+ # Search Complete - No Results Found
85
+
86
+ No content found matching '{query}' in the current project.
87
+
88
+ ## Suggestions to try:
89
+ 1. **Broaden your search**: Try fewer or more general terms
90
+ - Instead of: `{query}`
91
+ - Try: `{simplified_query}`
92
+
93
+ 2. **Check spelling**: Verify terms are spelled correctly
94
+ 3. **Try different search types**:
95
+ - Text search: `search_notes("{query}", search_type="text")`
96
+ - Title search: `search_notes("{query}", search_type="title")`
97
+ - Permalink search: `search_notes("{query}", search_type="permalink")`
98
+
99
+ 4. **Use boolean operators**:
100
+ - Try OR search for broader results
101
+
102
+ ## Check what content exists:
103
+ - Recent activity: `recent_activity(timeframe="7d")`
104
+ - List files: `list_directory("/")`
105
+ - Browse by folder: `list_directory("/notes")` or `list_directory("/docs")`
106
+ """).strip()
107
+
108
+ # Server/API errors
109
+ if "server error" in error_message.lower() or "internal" in error_message.lower():
110
+ return dedent(f"""
111
+ # Search Failed - Server Error
112
+
113
+ The search service encountered an error while processing '{query}': {error_message}
114
+
115
+ ## Immediate steps:
116
+ 1. **Try again**: The error might be temporary
117
+ 2. **Simplify the query**: Use simpler search terms
118
+ 3. **Check project status**: Ensure your project is properly synced
119
+
120
+ ## Alternative approaches:
121
+ - Browse files directly: `list_directory("/")`
122
+ - Check recent activity: `recent_activity(timeframe="7d")`
123
+ - Try a different search type: `search_notes("{query}", search_type="title")`
124
+
125
+ ## If the problem persists:
126
+ The search index might need to be rebuilt. Send a message to support@basicmachines.co or check the project sync status.
127
+ """).strip()
128
+
129
+ # Permission/access errors
130
+ if (
131
+ "permission" in error_message.lower()
132
+ or "access" in error_message.lower()
133
+ or "forbidden" in error_message.lower()
134
+ ):
135
+ return f"""# Search Failed - Access Error
136
+
137
+ You don't have permission to search in the current project: {error_message}
138
+
139
+ ## How to resolve:
140
+ 1. **Check your project access**: Verify you have read permissions for this project
141
+ 2. **Switch projects**: Try searching in a different project you have access to
142
+ 3. **Check authentication**: You might need to re-authenticate
143
+
144
+ ## Alternative actions:
145
+ - List available projects: `list_projects()`
146
+ - Switch to accessible project: `switch_project("project-name")`
147
+ - Check current project: `get_current_project()`"""
148
+
149
+ # Generic fallback
150
+ return f"""# Search Failed
151
+
152
+ Error searching for '{query}': {error_message}
153
+
154
+ ## General troubleshooting:
155
+ 1. **Check your query**: Ensure it uses valid search syntax
156
+ 2. **Try simpler terms**: Use basic words without special characters
157
+ 3. **Verify project access**: Make sure you can access the current project
158
+ 4. **Check recent activity**: `recent_activity(timeframe="7d")` to see if content exists
159
+
160
+ ## Alternative approaches:
161
+ - Browse files: `list_directory("/")`
162
+ - Try different search type: `search_notes("{query}", search_type="title")`
163
+ - Search with filters: `search_notes("{query}", types=["entity"])`
164
+
165
+ ## Need help?
166
+ - View recent changes: `recent_activity()`
167
+ - List projects: `list_projects()`
168
+ - Check current project: `get_current_project()`"""
169
+
170
+
13
171
  @mcp.tool(
14
172
  description="Search across all content in the knowledge base.",
15
173
  )
@@ -21,7 +179,8 @@ async def search_notes(
21
179
  types: Optional[List[str]] = None,
22
180
  entity_types: Optional[List[str]] = None,
23
181
  after_date: Optional[str] = None,
24
- ) -> SearchResponse:
182
+ project: Optional[str] = None,
183
+ ) -> SearchResponse | str:
25
184
  """Search across all content in the knowledge base.
26
185
 
27
186
  This tool searches the knowledge base using full-text search, pattern matching,
@@ -36,6 +195,7 @@ async def search_notes(
36
195
  types: Optional list of note types to search (e.g., ["note", "person"])
37
196
  entity_types: Optional list of entity types to filter by (e.g., ["entity", "observation"])
38
197
  after_date: Optional date filter for recent content (e.g., "1 week", "2d")
198
+ project: Optional project name to search in. If not provided, uses current active project.
39
199
 
40
200
  Returns:
41
201
  SearchResponse with results and pagination info
@@ -79,6 +239,9 @@ async def search_notes(
79
239
  query="docs/meeting-*",
80
240
  search_type="permalink"
81
241
  )
242
+
243
+ # Search in specific project
244
+ results = await search_notes("meeting notes", project="work-project")
82
245
  """
83
246
  # Create a SearchQuery object based on the parameters
84
247
  search_query = SearchQuery()
@@ -103,11 +266,29 @@ async def search_notes(
103
266
  if after_date:
104
267
  search_query.after_date = after_date
105
268
 
269
+ active_project = get_active_project(project)
270
+ project_url = active_project.project_url
271
+
106
272
  logger.info(f"Searching for {search_query}")
107
- response = await call_post(
108
- client,
109
- "/search/",
110
- json=search_query.model_dump(),
111
- params={"page": page, "page_size": page_size},
112
- )
113
- return SearchResponse.model_validate(response.json())
273
+
274
+ try:
275
+ response = await call_post(
276
+ client,
277
+ f"{project_url}/search/",
278
+ json=search_query.model_dump(),
279
+ params={"page": page, "page_size": page_size},
280
+ )
281
+ result = SearchResponse.model_validate(response.json())
282
+
283
+ # Check if we got no results and provide helpful guidance
284
+ if not result.results:
285
+ logger.info(f"Search returned no results for query: {query}")
286
+ # Don't treat this as an error, but the user might want guidance
287
+ # We return the empty result as normal - the user can decide if they need help
288
+
289
+ return result
290
+
291
+ except Exception as e:
292
+ logger.error(f"Search failed for query '{query}': {e}")
293
+ # Return formatted error message as string for better user experience
294
+ return _format_search_error_response(str(e), query, search_type)
@@ -0,0 +1,254 @@
1
+ """Sync status tool for Basic Memory MCP server."""
2
+
3
+ from typing import Optional
4
+
5
+ from loguru import logger
6
+
7
+ from basic_memory.mcp.server import mcp
8
+ from basic_memory.mcp.project_session import get_active_project
9
+
10
+
11
+ def _get_all_projects_status() -> list[str]:
12
+ """Get status lines for all configured projects."""
13
+ status_lines = []
14
+
15
+ try:
16
+ from basic_memory.config import app_config
17
+ from basic_memory.services.sync_status_service import sync_status_tracker
18
+
19
+ if app_config.projects:
20
+ status_lines.extend(["", "---", "", "**All Projects Status:**"])
21
+
22
+ for project_name, project_path in app_config.projects.items():
23
+ # Check if this project has sync status
24
+ project_sync_status = sync_status_tracker.get_project_status(project_name)
25
+
26
+ if project_sync_status:
27
+ # Project has tracked sync activity
28
+ if project_sync_status.status.value == "watching":
29
+ # Project is actively watching for changes (steady state)
30
+ status_icon = "👁️"
31
+ status_text = "Watching for changes"
32
+ elif project_sync_status.status.value == "completed":
33
+ # Sync completed but not yet watching - transitional state
34
+ status_icon = "✅"
35
+ status_text = "Sync completed"
36
+ elif project_sync_status.status.value in ["scanning", "syncing"]:
37
+ status_icon = "🔄"
38
+ status_text = "Sync in progress"
39
+ if project_sync_status.files_total > 0:
40
+ progress_pct = (
41
+ project_sync_status.files_processed
42
+ / project_sync_status.files_total
43
+ ) * 100
44
+ status_text += f" ({project_sync_status.files_processed}/{project_sync_status.files_total}, {progress_pct:.0f}%)"
45
+ elif project_sync_status.status.value == "failed":
46
+ status_icon = "❌"
47
+ status_text = f"Sync error: {project_sync_status.error or 'Unknown error'}"
48
+ else:
49
+ status_icon = "⏸️"
50
+ status_text = project_sync_status.status.value.title()
51
+ else:
52
+ # Project has no tracked sync activity - will be synced automatically
53
+ status_icon = "⏳"
54
+ status_text = "Pending sync"
55
+
56
+ status_lines.append(f"- {status_icon} **{project_name}**: {status_text}")
57
+
58
+ except Exception as e:
59
+ logger.debug(f"Could not get project config for comprehensive status: {e}")
60
+
61
+ return status_lines
62
+
63
+
64
+ @mcp.tool(
65
+ description="""Check the status of file synchronization and background operations.
66
+
67
+ Use this tool to:
68
+ - Check if file sync is in progress or completed
69
+ - Get detailed sync progress information
70
+ - Understand if your files are fully indexed
71
+ - Get specific error details if sync operations failed
72
+ - Monitor initial project setup and legacy migration
73
+
74
+ This covers all sync operations including:
75
+ - Initial project setup and file indexing
76
+ - Legacy project migration to unified database
77
+ - Ongoing file monitoring and updates
78
+ - Background processing of knowledge graphs
79
+ """,
80
+ )
81
+ async def sync_status(project: Optional[str] = None) -> str:
82
+ """Get current sync status and system readiness information.
83
+
84
+ This tool provides detailed information about any ongoing or completed
85
+ sync operations, helping users understand when their files are ready.
86
+
87
+ Args:
88
+ project: Optional project name to get project-specific context
89
+
90
+ Returns:
91
+ Formatted sync status with progress, readiness, and guidance
92
+ """
93
+ logger.info("MCP tool call tool=sync_status")
94
+
95
+ status_lines = []
96
+
97
+ try:
98
+ from basic_memory.services.sync_status_service import sync_status_tracker
99
+
100
+ # Get overall summary
101
+ summary = sync_status_tracker.get_summary()
102
+ is_ready = sync_status_tracker.is_ready
103
+
104
+ # Header
105
+ status_lines.extend(
106
+ [
107
+ "# Basic Memory Sync Status",
108
+ "",
109
+ f"**Current Status**: {summary}",
110
+ f"**System Ready**: {'✅ Yes' if is_ready else '🔄 Processing'}",
111
+ "",
112
+ ]
113
+ )
114
+
115
+ if is_ready:
116
+ status_lines.extend(
117
+ [
118
+ "✅ **All sync operations completed**",
119
+ "",
120
+ "- File indexing is complete",
121
+ "- Knowledge graphs are up to date",
122
+ "- All Basic Memory tools are fully operational",
123
+ "",
124
+ "Your knowledge base is ready for use!",
125
+ ]
126
+ )
127
+
128
+ # Show all projects status even when ready
129
+ status_lines.extend(_get_all_projects_status())
130
+ else:
131
+ # System is still processing - show both active and all projects
132
+ all_sync_projects = sync_status_tracker.get_all_projects()
133
+
134
+ active_projects = [
135
+ p for p in all_sync_projects.values() if p.status.value in ["scanning", "syncing"]
136
+ ]
137
+ failed_projects = [p for p in all_sync_projects.values() if p.status.value == "failed"]
138
+
139
+ if active_projects:
140
+ status_lines.extend(
141
+ [
142
+ "🔄 **File synchronization in progress**",
143
+ "",
144
+ "Basic Memory is automatically processing all configured projects and building knowledge graphs.",
145
+ "This typically takes 1-3 minutes depending on the amount of content.",
146
+ "",
147
+ "**Currently Processing:**",
148
+ ]
149
+ )
150
+
151
+ for project_status in active_projects:
152
+ progress = ""
153
+ if project_status.files_total > 0:
154
+ progress_pct = (
155
+ project_status.files_processed / project_status.files_total
156
+ ) * 100
157
+ progress = f" ({project_status.files_processed}/{project_status.files_total}, {progress_pct:.0f}%)"
158
+
159
+ status_lines.append(
160
+ f"- **{project_status.project_name}**: {project_status.message}{progress}"
161
+ )
162
+
163
+ status_lines.extend(
164
+ [
165
+ "",
166
+ "**What's happening:**",
167
+ "- Scanning and indexing markdown files",
168
+ "- Building entity and relationship graphs",
169
+ "- Setting up full-text search indexes",
170
+ "- Processing file changes and updates",
171
+ "",
172
+ "**What you can do:**",
173
+ "- Wait for automatic processing to complete - no action needed",
174
+ "- Use this tool again to check progress",
175
+ "- Simple operations may work already",
176
+ "- All projects will be available once sync finishes",
177
+ ]
178
+ )
179
+
180
+ # Handle failed projects (independent of active projects)
181
+ if failed_projects:
182
+ status_lines.extend(["", "❌ **Some projects failed to sync:**", ""])
183
+
184
+ for project_status in failed_projects:
185
+ status_lines.append(
186
+ f"- **{project_status.project_name}**: {project_status.error or 'Unknown error'}"
187
+ )
188
+
189
+ status_lines.extend(
190
+ [
191
+ "",
192
+ "**Next steps:**",
193
+ "1. Check the logs for detailed error information",
194
+ "2. Ensure file permissions allow read/write access",
195
+ "3. Try restarting the MCP server",
196
+ "4. If issues persist, consider filing a support issue",
197
+ ]
198
+ )
199
+ elif not active_projects:
200
+ # No active or failed projects - must be pending
201
+ status_lines.extend(
202
+ [
203
+ "⏳ **Sync operations pending**",
204
+ "",
205
+ "File synchronization has been queued but hasn't started yet.",
206
+ "This usually resolves automatically within a few seconds.",
207
+ ]
208
+ )
209
+
210
+ # Add comprehensive project status for all configured projects
211
+ all_projects_status = _get_all_projects_status()
212
+ if all_projects_status:
213
+ status_lines.extend(all_projects_status)
214
+
215
+ # Add explanation about automatic syncing if there are unsynced projects
216
+ unsynced_count = sum(1 for line in all_projects_status if "⏳" in line)
217
+ if unsynced_count > 0 and not is_ready:
218
+ status_lines.extend(
219
+ [
220
+ "",
221
+ "**Note**: All configured projects will be automatically synced during startup.",
222
+ "You don't need to manually switch projects - Basic Memory handles this for you.",
223
+ ]
224
+ )
225
+
226
+ # Add project context if provided
227
+ if project:
228
+ try:
229
+ active_project = get_active_project(project)
230
+ status_lines.extend(
231
+ [
232
+ "",
233
+ "---",
234
+ "",
235
+ f"**Active Project**: {active_project.name}",
236
+ f"**Project Path**: {active_project.home}",
237
+ ]
238
+ )
239
+ except Exception as e:
240
+ logger.debug(f"Could not get project info: {e}")
241
+
242
+ return "\n".join(status_lines)
243
+
244
+ except Exception as e:
245
+ return f"""# Sync Status - Error
246
+
247
+ ❌ **Unable to check sync status**: {str(e)}
248
+
249
+ **Troubleshooting:**
250
+ - The system may still be starting up
251
+ - Try waiting a few seconds and checking again
252
+ - Check logs for detailed error information
253
+ - Consider restarting if the issue persists
254
+ """