basic-memory 0.13.0b4__py3-none-any.whl → 0.13.0b6__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.
- basic_memory/__init__.py +2 -7
- basic_memory/api/routers/knowledge_router.py +13 -0
- basic_memory/api/routers/memory_router.py +3 -4
- basic_memory/api/routers/project_router.py +6 -5
- basic_memory/api/routers/prompt_router.py +2 -2
- basic_memory/cli/commands/project.py +3 -3
- basic_memory/cli/commands/status.py +1 -1
- basic_memory/cli/commands/sync.py +1 -1
- basic_memory/cli/commands/tool.py +6 -6
- basic_memory/mcp/prompts/__init__.py +2 -0
- basic_memory/mcp/prompts/recent_activity.py +1 -1
- basic_memory/mcp/prompts/sync_status.py +116 -0
- basic_memory/mcp/server.py +6 -6
- basic_memory/mcp/tools/__init__.py +4 -0
- basic_memory/mcp/tools/build_context.py +32 -7
- basic_memory/mcp/tools/canvas.py +2 -1
- basic_memory/mcp/tools/delete_note.py +159 -4
- basic_memory/mcp/tools/edit_note.py +17 -11
- basic_memory/mcp/tools/move_note.py +252 -40
- basic_memory/mcp/tools/project_management.py +35 -3
- basic_memory/mcp/tools/read_note.py +11 -4
- basic_memory/mcp/tools/search.py +180 -8
- basic_memory/mcp/tools/sync_status.py +254 -0
- basic_memory/mcp/tools/utils.py +47 -0
- basic_memory/mcp/tools/view_note.py +66 -0
- basic_memory/mcp/tools/write_note.py +13 -2
- basic_memory/repository/search_repository.py +116 -38
- basic_memory/schemas/base.py +33 -5
- basic_memory/schemas/memory.py +58 -1
- basic_memory/services/entity_service.py +18 -5
- basic_memory/services/initialization.py +32 -5
- basic_memory/services/link_resolver.py +20 -5
- basic_memory/services/migration_service.py +168 -0
- basic_memory/services/project_service.py +121 -50
- basic_memory/services/sync_status_service.py +181 -0
- basic_memory/sync/sync_service.py +91 -13
- {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b6.dist-info}/METADATA +2 -2
- {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b6.dist-info}/RECORD +41 -36
- {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b6.dist-info}/WHEEL +0 -0
- {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b6.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b6.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
from textwrap import dedent
|
|
1
2
|
from typing import Optional
|
|
2
3
|
|
|
4
|
+
from loguru import logger
|
|
5
|
+
|
|
3
6
|
from basic_memory.mcp.tools.utils import call_delete
|
|
4
7
|
from basic_memory.mcp.server import mcp
|
|
5
8
|
from basic_memory.mcp.async_client import client
|
|
@@ -7,8 +10,148 @@ from basic_memory.mcp.project_session import get_active_project
|
|
|
7
10
|
from basic_memory.schemas import DeleteEntitiesResponse
|
|
8
11
|
|
|
9
12
|
|
|
13
|
+
def _format_delete_error_response(error_message: str, identifier: str) -> str:
|
|
14
|
+
"""Format helpful error responses for delete failures that guide users to successful deletions."""
|
|
15
|
+
|
|
16
|
+
# Note not found errors
|
|
17
|
+
if "entity not found" in error_message.lower() or "not found" in error_message.lower():
|
|
18
|
+
search_term = identifier.split("/")[-1] if "/" in identifier else identifier
|
|
19
|
+
title_format = (
|
|
20
|
+
identifier.split("/")[-1].replace("-", " ").title() if "/" in identifier else identifier
|
|
21
|
+
)
|
|
22
|
+
permalink_format = identifier.lower().replace(" ", "-")
|
|
23
|
+
|
|
24
|
+
return dedent(f"""
|
|
25
|
+
# Delete Failed - Note Not Found
|
|
26
|
+
|
|
27
|
+
The note '{identifier}' could not be found for deletion.
|
|
28
|
+
|
|
29
|
+
## This might mean:
|
|
30
|
+
1. **Already deleted**: The note may have been deleted previously
|
|
31
|
+
2. **Wrong identifier**: The identifier format might be incorrect
|
|
32
|
+
3. **Different project**: The note might be in a different project
|
|
33
|
+
|
|
34
|
+
## How to verify:
|
|
35
|
+
1. **Search for the note**: Use `search_notes("{search_term}")` to find it
|
|
36
|
+
2. **Try different formats**:
|
|
37
|
+
- If you used a permalink like "folder/note-title", try just the title: "{title_format}"
|
|
38
|
+
- If you used a title, try the permalink format: "{permalink_format}"
|
|
39
|
+
|
|
40
|
+
3. **Check if already deleted**: Use `list_directory("/")` to see what notes exist
|
|
41
|
+
4. **Check current project**: Use `get_current_project()` to verify you're in the right project
|
|
42
|
+
|
|
43
|
+
## If the note actually exists:
|
|
44
|
+
```
|
|
45
|
+
# First, find the correct identifier:
|
|
46
|
+
search_notes("{identifier}")
|
|
47
|
+
|
|
48
|
+
# Then delete using the correct identifier:
|
|
49
|
+
delete_note("correct-identifier-from-search")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## If you want to delete multiple similar notes:
|
|
53
|
+
Use search to find all related notes and delete them one by one.
|
|
54
|
+
""").strip()
|
|
55
|
+
|
|
56
|
+
# Permission/access errors
|
|
57
|
+
if (
|
|
58
|
+
"permission" in error_message.lower()
|
|
59
|
+
or "access" in error_message.lower()
|
|
60
|
+
or "forbidden" in error_message.lower()
|
|
61
|
+
):
|
|
62
|
+
return f"""# Delete Failed - Permission Error
|
|
63
|
+
|
|
64
|
+
You don't have permission to delete '{identifier}': {error_message}
|
|
65
|
+
|
|
66
|
+
## How to resolve:
|
|
67
|
+
1. **Check permissions**: Verify you have delete/write access to this project
|
|
68
|
+
2. **File locks**: The note might be open in another application
|
|
69
|
+
3. **Project access**: Ensure you're in the correct project with proper permissions
|
|
70
|
+
|
|
71
|
+
## Alternative actions:
|
|
72
|
+
- Check current project: `get_current_project()`
|
|
73
|
+
- Switch to correct project: `switch_project("project-name")`
|
|
74
|
+
- Verify note exists first: `read_note("{identifier}")`
|
|
75
|
+
|
|
76
|
+
## If you have read-only access:
|
|
77
|
+
Send a message to support@basicmachines.co to request deletion, or ask someone with write access to delete the note."""
|
|
78
|
+
|
|
79
|
+
# Server/filesystem errors
|
|
80
|
+
if (
|
|
81
|
+
"server error" in error_message.lower()
|
|
82
|
+
or "filesystem" in error_message.lower()
|
|
83
|
+
or "disk" in error_message.lower()
|
|
84
|
+
):
|
|
85
|
+
return f"""# Delete Failed - System Error
|
|
86
|
+
|
|
87
|
+
A system error occurred while deleting '{identifier}': {error_message}
|
|
88
|
+
|
|
89
|
+
## Immediate steps:
|
|
90
|
+
1. **Try again**: The error might be temporary
|
|
91
|
+
2. **Check file status**: Verify the file isn't locked or in use
|
|
92
|
+
3. **Check disk space**: Ensure the system has adequate storage
|
|
93
|
+
|
|
94
|
+
## Troubleshooting:
|
|
95
|
+
- Verify note exists: `read_note("{identifier}")`
|
|
96
|
+
- Check project status: `get_current_project()`
|
|
97
|
+
- Try again in a few moments
|
|
98
|
+
|
|
99
|
+
## If problem persists:
|
|
100
|
+
Send a message to support@basicmachines.co - there may be a filesystem or database issue."""
|
|
101
|
+
|
|
102
|
+
# Database/sync errors
|
|
103
|
+
if "database" in error_message.lower() or "sync" in error_message.lower():
|
|
104
|
+
return f"""# Delete Failed - Database Error
|
|
105
|
+
|
|
106
|
+
A database error occurred while deleting '{identifier}': {error_message}
|
|
107
|
+
|
|
108
|
+
## This usually means:
|
|
109
|
+
1. **Sync conflict**: The file system and database are out of sync
|
|
110
|
+
2. **Database lock**: Another operation is accessing the database
|
|
111
|
+
3. **Corrupted entry**: The database entry might be corrupted
|
|
112
|
+
|
|
113
|
+
## Steps to resolve:
|
|
114
|
+
1. **Try again**: Wait a moment and retry the deletion
|
|
115
|
+
2. **Check note status**: `read_note("{identifier}")` to see current state
|
|
116
|
+
3. **Manual verification**: Use `list_directory()` to see if file still exists
|
|
117
|
+
|
|
118
|
+
## If the note appears gone but database shows it exists:
|
|
119
|
+
Send a message to support@basicmachines.co - a manual database cleanup may be needed."""
|
|
120
|
+
|
|
121
|
+
# Generic fallback
|
|
122
|
+
return f"""# Delete Failed
|
|
123
|
+
|
|
124
|
+
Error deleting note '{identifier}': {error_message}
|
|
125
|
+
|
|
126
|
+
## General troubleshooting:
|
|
127
|
+
1. **Verify the note exists**: `read_note("{identifier}")` or `search_notes("{identifier}")`
|
|
128
|
+
2. **Check permissions**: Ensure you can edit/delete files in this project
|
|
129
|
+
3. **Try again**: The error might be temporary
|
|
130
|
+
4. **Check project**: Make sure you're in the correct project
|
|
131
|
+
|
|
132
|
+
## Step-by-step approach:
|
|
133
|
+
```
|
|
134
|
+
# 1. Confirm note exists and get correct identifier
|
|
135
|
+
search_notes("{identifier}")
|
|
136
|
+
|
|
137
|
+
# 2. Read the note to verify access
|
|
138
|
+
read_note("correct-identifier-from-search")
|
|
139
|
+
|
|
140
|
+
# 3. Try deletion with correct identifier
|
|
141
|
+
delete_note("correct-identifier-from-search")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Alternative approaches:
|
|
145
|
+
- Check what notes exist: `list_directory("/")`
|
|
146
|
+
- Verify current project: `get_current_project()`
|
|
147
|
+
- Switch projects if needed: `switch_project("correct-project")`
|
|
148
|
+
|
|
149
|
+
## Need help?
|
|
150
|
+
If the note should be deleted but the operation keeps failing, send a message to support@basicmachines.co."""
|
|
151
|
+
|
|
152
|
+
|
|
10
153
|
@mcp.tool(description="Delete a note by title or permalink")
|
|
11
|
-
async def delete_note(identifier: str, project: Optional[str] = None) -> bool:
|
|
154
|
+
async def delete_note(identifier: str, project: Optional[str] = None) -> bool | str:
|
|
12
155
|
"""Delete a note from the knowledge base.
|
|
13
156
|
|
|
14
157
|
Args:
|
|
@@ -31,6 +174,18 @@ async def delete_note(identifier: str, project: Optional[str] = None) -> bool:
|
|
|
31
174
|
active_project = get_active_project(project)
|
|
32
175
|
project_url = active_project.project_url
|
|
33
176
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
177
|
+
try:
|
|
178
|
+
response = await call_delete(client, f"{project_url}/knowledge/entities/{identifier}")
|
|
179
|
+
result = DeleteEntitiesResponse.model_validate(response.json())
|
|
180
|
+
|
|
181
|
+
if result.deleted:
|
|
182
|
+
logger.info(f"Successfully deleted note: {identifier}")
|
|
183
|
+
return True
|
|
184
|
+
else:
|
|
185
|
+
logger.warning(f"Delete operation completed but note was not deleted: {identifier}")
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
except Exception as e: # pragma: no cover
|
|
189
|
+
logger.error(f"Delete failed for '{identifier}': {e}")
|
|
190
|
+
# Return formatted error message for better user experience
|
|
191
|
+
return _format_delete_error_response(str(e), identifier)
|
|
@@ -24,14 +24,14 @@ def _format_error_response(
|
|
|
24
24
|
if "Entity not found" in error_message or "entity not found" in error_message.lower():
|
|
25
25
|
return f"""# Edit Failed - Note Not Found
|
|
26
26
|
|
|
27
|
-
The note with identifier '{identifier}' could not be found.
|
|
27
|
+
The note with identifier '{identifier}' could not be found. Edit operations require an exact match (no fuzzy matching).
|
|
28
28
|
|
|
29
29
|
## Suggestions to try:
|
|
30
|
-
1. **Search for the note first**: Use `search_notes("{identifier.split("/")[-1]}")` to find similar notes
|
|
31
|
-
2. **Try different identifier formats**:
|
|
32
|
-
- If you used a permalink like "folder/note-title", try
|
|
33
|
-
- If you used a title, try the permalink format: "{identifier.lower().replace(" ", "-")}"
|
|
34
|
-
- Use `read_note()` first to verify the note exists and get the
|
|
30
|
+
1. **Search for the note first**: Use `search_notes("{identifier.split("/")[-1]}")` to find similar notes with exact identifiers
|
|
31
|
+
2. **Try different exact identifier formats**:
|
|
32
|
+
- If you used a permalink like "folder/note-title", try the exact title: "{identifier.split("/")[-1].replace("-", " ").title()}"
|
|
33
|
+
- If you used a title, try the exact permalink format: "{identifier.lower().replace(" ", "-")}"
|
|
34
|
+
- Use `read_note()` first to verify the note exists and get the exact identifier
|
|
35
35
|
|
|
36
36
|
## Alternative approach:
|
|
37
37
|
Use `write_note()` to create the note first, then edit it."""
|
|
@@ -142,7 +142,9 @@ async def edit_note(
|
|
|
142
142
|
It supports various operations for different editing scenarios.
|
|
143
143
|
|
|
144
144
|
Args:
|
|
145
|
-
identifier: The title, permalink, or memory:// URL of the note to edit
|
|
145
|
+
identifier: The exact title, permalink, or memory:// URL of the note to edit.
|
|
146
|
+
Must be an exact match - fuzzy matching is not supported for edit operations.
|
|
147
|
+
Use search_notes() or read_note() first to find the correct identifier if uncertain.
|
|
146
148
|
operation: The editing operation to perform:
|
|
147
149
|
- "append": Add content to the end of the note
|
|
148
150
|
- "prepend": Add content to the beginning of the note
|
|
@@ -179,10 +181,14 @@ async def edit_note(
|
|
|
179
181
|
# Replace subsection with more specific header
|
|
180
182
|
edit_note("docs/setup", "replace_section", "Updated install steps\\n", section="### Installation")
|
|
181
183
|
|
|
182
|
-
# Using different identifier formats
|
|
183
|
-
edit_note("Meeting Notes", "append", "\\n- Follow up on action items") # title
|
|
184
|
-
edit_note("docs/meeting-notes", "append", "\\n- Follow up tasks") # permalink
|
|
185
|
-
edit_note("docs/Meeting Notes", "append", "\\n- Next steps") # folder/title
|
|
184
|
+
# Using different identifier formats (must be exact matches)
|
|
185
|
+
edit_note("Meeting Notes", "append", "\\n- Follow up on action items") # exact title
|
|
186
|
+
edit_note("docs/meeting-notes", "append", "\\n- Follow up tasks") # exact permalink
|
|
187
|
+
edit_note("docs/Meeting Notes", "append", "\\n- Next steps") # exact folder/title
|
|
188
|
+
|
|
189
|
+
# If uncertain about identifier, search first:
|
|
190
|
+
# search_notes("meeting") # Find available notes
|
|
191
|
+
# edit_note("docs/meeting-notes-2025", "append", "content") # Use exact result
|
|
186
192
|
|
|
187
193
|
# Add new section to document
|
|
188
194
|
edit_note("project-plan", "replace_section", "TBD - needs research\\n", section="## Future Work")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Move note tool for Basic Memory MCP server."""
|
|
2
2
|
|
|
3
|
+
from textwrap import dedent
|
|
3
4
|
from typing import Optional
|
|
4
5
|
|
|
5
6
|
from loguru import logger
|
|
@@ -11,6 +12,203 @@ from basic_memory.mcp.project_session import get_active_project
|
|
|
11
12
|
from basic_memory.schemas import EntityResponse
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
def _format_move_error_response(error_message: str, identifier: str, destination_path: str) -> str:
|
|
16
|
+
"""Format helpful error responses for move failures that guide users to successful moves."""
|
|
17
|
+
|
|
18
|
+
# Note not found errors
|
|
19
|
+
if "entity not found" in error_message.lower() or "not found" in error_message.lower():
|
|
20
|
+
search_term = identifier.split("/")[-1] if "/" in identifier else identifier
|
|
21
|
+
title_format = (
|
|
22
|
+
identifier.split("/")[-1].replace("-", " ").title() if "/" in identifier else identifier
|
|
23
|
+
)
|
|
24
|
+
permalink_format = identifier.lower().replace(" ", "-")
|
|
25
|
+
|
|
26
|
+
return dedent(f"""
|
|
27
|
+
# Move Failed - Note Not Found
|
|
28
|
+
|
|
29
|
+
The note '{identifier}' could not be found for moving. Move operations require an exact match (no fuzzy matching).
|
|
30
|
+
|
|
31
|
+
## Suggestions to try:
|
|
32
|
+
1. **Search for the note first**: Use `search_notes("{search_term}")` to find it with exact identifiers
|
|
33
|
+
2. **Try different exact identifier formats**:
|
|
34
|
+
- If you used a permalink like "folder/note-title", try the exact title: "{title_format}"
|
|
35
|
+
- If you used a title, try the exact permalink format: "{permalink_format}"
|
|
36
|
+
- Use `read_note()` first to verify the note exists and get the exact identifier
|
|
37
|
+
|
|
38
|
+
3. **Check current project**: Use `get_current_project()` to verify you're in the right project
|
|
39
|
+
4. **List available notes**: Use `list_directory("/")` to see what notes exist
|
|
40
|
+
|
|
41
|
+
## Before trying again:
|
|
42
|
+
```
|
|
43
|
+
# First, verify the note exists:
|
|
44
|
+
search_notes("{identifier}")
|
|
45
|
+
|
|
46
|
+
# Then use the exact identifier from search results:
|
|
47
|
+
move_note("correct-identifier-here", "{destination_path}")
|
|
48
|
+
```
|
|
49
|
+
""").strip()
|
|
50
|
+
|
|
51
|
+
# Destination already exists errors
|
|
52
|
+
if "already exists" in error_message.lower() or "file exists" in error_message.lower():
|
|
53
|
+
return f"""# Move Failed - Destination Already Exists
|
|
54
|
+
|
|
55
|
+
Cannot move '{identifier}' to '{destination_path}' because a file already exists at that location.
|
|
56
|
+
|
|
57
|
+
## How to resolve:
|
|
58
|
+
1. **Choose a different destination**: Try a different filename or folder
|
|
59
|
+
- Add timestamp: `{destination_path.rsplit(".", 1)[0] if "." in destination_path else destination_path}-backup.md`
|
|
60
|
+
- Use different folder: `archive/{destination_path}` or `backup/{destination_path}`
|
|
61
|
+
|
|
62
|
+
2. **Check the existing file**: Use `read_note("{destination_path}")` to see what's already there
|
|
63
|
+
3. **Remove or rename existing**: If safe to do so, move the existing file first
|
|
64
|
+
|
|
65
|
+
## Try these alternatives:
|
|
66
|
+
```
|
|
67
|
+
# Option 1: Add timestamp to make unique
|
|
68
|
+
move_note("{identifier}", "{destination_path.rsplit(".", 1)[0] if "." in destination_path else destination_path}-backup.md")
|
|
69
|
+
|
|
70
|
+
# Option 2: Use archive folder
|
|
71
|
+
move_note("{identifier}", "archive/{destination_path}")
|
|
72
|
+
|
|
73
|
+
# Option 3: Check what's at destination first
|
|
74
|
+
read_note("{destination_path}")
|
|
75
|
+
```"""
|
|
76
|
+
|
|
77
|
+
# Invalid path errors
|
|
78
|
+
if "invalid" in error_message.lower() and "path" in error_message.lower():
|
|
79
|
+
return f"""# Move Failed - Invalid Destination Path
|
|
80
|
+
|
|
81
|
+
The destination path '{destination_path}' is not valid: {error_message}
|
|
82
|
+
|
|
83
|
+
## Path requirements:
|
|
84
|
+
1. **Relative paths only**: Don't start with `/` (use `notes/file.md` not `/notes/file.md`)
|
|
85
|
+
2. **Include file extension**: Add `.md` for markdown files
|
|
86
|
+
3. **Use forward slashes**: For folder separators (`folder/subfolder/file.md`)
|
|
87
|
+
4. **No special characters**: Avoid `\\`, `:`, `*`, `?`, `"`, `<`, `>`, `|`
|
|
88
|
+
|
|
89
|
+
## Valid path examples:
|
|
90
|
+
- `notes/my-note.md`
|
|
91
|
+
- `projects/2025/meeting-notes.md`
|
|
92
|
+
- `archive/old-projects/legacy-note.md`
|
|
93
|
+
|
|
94
|
+
## Try again with:
|
|
95
|
+
```
|
|
96
|
+
move_note("{identifier}", "notes/{destination_path.split("/")[-1] if "/" in destination_path else destination_path}")
|
|
97
|
+
```"""
|
|
98
|
+
|
|
99
|
+
# Permission/access errors
|
|
100
|
+
if (
|
|
101
|
+
"permission" in error_message.lower()
|
|
102
|
+
or "access" in error_message.lower()
|
|
103
|
+
or "forbidden" in error_message.lower()
|
|
104
|
+
):
|
|
105
|
+
return f"""# Move Failed - Permission Error
|
|
106
|
+
|
|
107
|
+
You don't have permission to move '{identifier}': {error_message}
|
|
108
|
+
|
|
109
|
+
## How to resolve:
|
|
110
|
+
1. **Check file permissions**: Ensure you have write access to both source and destination
|
|
111
|
+
2. **Verify project access**: Make sure you have edit permissions for this project
|
|
112
|
+
3. **Check file locks**: The file might be open in another application
|
|
113
|
+
|
|
114
|
+
## Alternative actions:
|
|
115
|
+
- Check current project: `get_current_project()`
|
|
116
|
+
- Switch projects if needed: `switch_project("project-name")`
|
|
117
|
+
- Try copying content instead: `read_note("{identifier}")` then `write_note()` to new location"""
|
|
118
|
+
|
|
119
|
+
# Source file not found errors
|
|
120
|
+
if "source" in error_message.lower() and (
|
|
121
|
+
"not found" in error_message.lower() or "missing" in error_message.lower()
|
|
122
|
+
):
|
|
123
|
+
return f"""# Move Failed - Source File Missing
|
|
124
|
+
|
|
125
|
+
The source file for '{identifier}' was not found on disk: {error_message}
|
|
126
|
+
|
|
127
|
+
This usually means the database and filesystem are out of sync.
|
|
128
|
+
|
|
129
|
+
## How to resolve:
|
|
130
|
+
1. **Check if note exists in database**: `read_note("{identifier}")`
|
|
131
|
+
2. **Run sync operation**: The file might need to be re-synced
|
|
132
|
+
3. **Recreate the file**: If data exists in database, recreate the physical file
|
|
133
|
+
|
|
134
|
+
## Troubleshooting steps:
|
|
135
|
+
```
|
|
136
|
+
# Check if note exists in Basic Memory
|
|
137
|
+
read_note("{identifier}")
|
|
138
|
+
|
|
139
|
+
# If it exists, the file is missing on disk - send a message to support@basicmachines.co
|
|
140
|
+
# If it doesn't exist, use search to find the correct identifier
|
|
141
|
+
search_notes("{identifier}")
|
|
142
|
+
```"""
|
|
143
|
+
|
|
144
|
+
# Server/filesystem errors
|
|
145
|
+
if (
|
|
146
|
+
"server error" in error_message.lower()
|
|
147
|
+
or "filesystem" in error_message.lower()
|
|
148
|
+
or "disk" in error_message.lower()
|
|
149
|
+
):
|
|
150
|
+
return f"""# Move Failed - System Error
|
|
151
|
+
|
|
152
|
+
A system error occurred while moving '{identifier}': {error_message}
|
|
153
|
+
|
|
154
|
+
## Immediate steps:
|
|
155
|
+
1. **Try again**: The error might be temporary
|
|
156
|
+
2. **Check disk space**: Ensure adequate storage is available
|
|
157
|
+
3. **Verify filesystem permissions**: Check if the destination directory is writable
|
|
158
|
+
|
|
159
|
+
## Alternative approaches:
|
|
160
|
+
- Copy content to new location: Use `read_note("{identifier}")` then `write_note()`
|
|
161
|
+
- Use a different destination folder that you know works
|
|
162
|
+
- Send a message to support@basicmachines.co if the problem persists
|
|
163
|
+
|
|
164
|
+
## Backup approach:
|
|
165
|
+
```
|
|
166
|
+
# Read current content
|
|
167
|
+
content = read_note("{identifier}")
|
|
168
|
+
|
|
169
|
+
# Create new note at desired location
|
|
170
|
+
write_note("New Note Title", content, "{destination_path.split("/")[0] if "/" in destination_path else "notes"}")
|
|
171
|
+
|
|
172
|
+
# Then delete original if successful
|
|
173
|
+
delete_note("{identifier}")
|
|
174
|
+
```"""
|
|
175
|
+
|
|
176
|
+
# Generic fallback
|
|
177
|
+
return f"""# Move Failed
|
|
178
|
+
|
|
179
|
+
Error moving '{identifier}' to '{destination_path}': {error_message}
|
|
180
|
+
|
|
181
|
+
## General troubleshooting:
|
|
182
|
+
1. **Verify the note exists**: `read_note("{identifier}")` or `search_notes("{identifier}")`
|
|
183
|
+
2. **Check destination path**: Ensure it's a valid relative path with `.md` extension
|
|
184
|
+
3. **Verify permissions**: Make sure you can edit files in this project
|
|
185
|
+
4. **Try a simpler path**: Use a basic folder structure like `notes/filename.md`
|
|
186
|
+
|
|
187
|
+
## Step-by-step approach:
|
|
188
|
+
```
|
|
189
|
+
# 1. Confirm note exists
|
|
190
|
+
read_note("{identifier}")
|
|
191
|
+
|
|
192
|
+
# 2. Try a simple destination first
|
|
193
|
+
move_note("{identifier}", "notes/{destination_path.split("/")[-1] if "/" in destination_path else destination_path}")
|
|
194
|
+
|
|
195
|
+
# 3. If that works, then try your original destination
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Alternative approach:
|
|
199
|
+
If moving continues to fail, you can copy the content manually:
|
|
200
|
+
```
|
|
201
|
+
# Read current content
|
|
202
|
+
content = read_note("{identifier}")
|
|
203
|
+
|
|
204
|
+
# Create new note
|
|
205
|
+
write_note("Title", content, "target-folder")
|
|
206
|
+
|
|
207
|
+
# Delete original once confirmed
|
|
208
|
+
delete_note("{identifier}")
|
|
209
|
+
```"""
|
|
210
|
+
|
|
211
|
+
|
|
14
212
|
@mcp.tool(
|
|
15
213
|
description="Move a note to a new location, updating database and maintaining links.",
|
|
16
214
|
)
|
|
@@ -22,7 +220,9 @@ async def move_note(
|
|
|
22
220
|
"""Move a note to a new file location within the same project.
|
|
23
221
|
|
|
24
222
|
Args:
|
|
25
|
-
identifier:
|
|
223
|
+
identifier: Exact entity identifier (title, permalink, or memory:// URL).
|
|
224
|
+
Must be an exact match - fuzzy matching is not supported for move operations.
|
|
225
|
+
Use search_notes() or read_note() first to find the correct identifier if uncertain.
|
|
26
226
|
destination_path: New path relative to project root (e.g., "work/meetings/2025-05-26.md")
|
|
27
227
|
project: Optional project name (defaults to current session project)
|
|
28
228
|
|
|
@@ -30,9 +230,18 @@ async def move_note(
|
|
|
30
230
|
Success message with move details
|
|
31
231
|
|
|
32
232
|
Examples:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
233
|
+
# Move to new folder (exact title match)
|
|
234
|
+
move_note("My Note", "work/notes/my-note.md")
|
|
235
|
+
|
|
236
|
+
# Move by exact permalink
|
|
237
|
+
move_note("my-note-permalink", "archive/old-notes/my-note.md")
|
|
238
|
+
|
|
239
|
+
# Specify project with exact identifier
|
|
240
|
+
move_note("My Note", "archive/my-note.md", project="work-project")
|
|
241
|
+
|
|
242
|
+
# If uncertain about identifier, search first:
|
|
243
|
+
# search_notes("my note") # Find available notes
|
|
244
|
+
# move_note("docs/my-note-2025", "archive/my-note.md") # Use exact result
|
|
36
245
|
|
|
37
246
|
Note: This operation moves notes within the specified project only. Moving notes
|
|
38
247
|
between different projects is not currently supported.
|
|
@@ -49,39 +258,42 @@ async def move_note(
|
|
|
49
258
|
active_project = get_active_project(project)
|
|
50
259
|
project_url = active_project.project_url
|
|
51
260
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
261
|
+
try:
|
|
262
|
+
# Prepare move request
|
|
263
|
+
move_data = {
|
|
264
|
+
"identifier": identifier,
|
|
265
|
+
"destination_path": destination_path,
|
|
266
|
+
"project": active_project.name,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# Call the move API endpoint
|
|
270
|
+
url = f"{project_url}/knowledge/move"
|
|
271
|
+
response = await call_post(client, url, json=move_data)
|
|
272
|
+
result = EntityResponse.model_validate(response.json())
|
|
273
|
+
|
|
274
|
+
# Build success message
|
|
275
|
+
result_lines = [
|
|
276
|
+
"✅ Note moved successfully",
|
|
277
|
+
"",
|
|
278
|
+
f"📁 **{identifier}** → **{result.file_path}**",
|
|
279
|
+
f"🔗 Permalink: {result.permalink}",
|
|
280
|
+
"📊 Database and search index updated",
|
|
281
|
+
"",
|
|
282
|
+
f"<!-- Project: {active_project.name} -->",
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
# Log the operation
|
|
286
|
+
logger.info(
|
|
287
|
+
"Move note completed",
|
|
288
|
+
identifier=identifier,
|
|
289
|
+
destination_path=destination_path,
|
|
290
|
+
project=active_project.name,
|
|
291
|
+
status_code=response.status_code,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
return "\n".join(result_lines)
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
logger.error(f"Move failed for '{identifier}' to '{destination_path}': {e}")
|
|
298
|
+
# Return formatted error message for better user experience
|
|
299
|
+
return _format_move_error_response(str(e), identifier, destination_path)
|
|
@@ -4,6 +4,8 @@ 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
|
+
|
|
7
9
|
from fastmcp import Context
|
|
8
10
|
from loguru import logger
|
|
9
11
|
|
|
@@ -94,7 +96,11 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str:
|
|
|
94
96
|
|
|
95
97
|
# Get project info to show summary
|
|
96
98
|
try:
|
|
97
|
-
response = await call_get(
|
|
99
|
+
response = await call_get(
|
|
100
|
+
client,
|
|
101
|
+
f"{project_config.project_url}/project/info",
|
|
102
|
+
params={"project_name": project_name},
|
|
103
|
+
)
|
|
98
104
|
project_info = ProjectInfoResponse.model_validate(response.json())
|
|
99
105
|
|
|
100
106
|
result = f"✓ Switched to {project_name} project\n\n"
|
|
@@ -115,7 +121,29 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str:
|
|
|
115
121
|
logger.error(f"Error switching to project {project_name}: {e}")
|
|
116
122
|
# Revert to previous project on error
|
|
117
123
|
session.set_current_project(current_project)
|
|
118
|
-
|
|
124
|
+
|
|
125
|
+
# Return user-friendly error message instead of raising exception
|
|
126
|
+
return dedent(f"""
|
|
127
|
+
# Project Switch Failed
|
|
128
|
+
|
|
129
|
+
Could not switch to project '{project_name}': {str(e)}
|
|
130
|
+
|
|
131
|
+
## Current project: {current_project}
|
|
132
|
+
Your session remains on the previous project.
|
|
133
|
+
|
|
134
|
+
## Troubleshooting:
|
|
135
|
+
1. **Check available projects**: Use `list_projects()` to see valid project names
|
|
136
|
+
2. **Verify spelling**: Ensure the project name is spelled correctly
|
|
137
|
+
3. **Check permissions**: Verify you have access to the requested project
|
|
138
|
+
4. **Try again**: The error might be temporary
|
|
139
|
+
|
|
140
|
+
## Available options:
|
|
141
|
+
- See all projects: `list_projects()`
|
|
142
|
+
- Stay on current project: `get_current_project()`
|
|
143
|
+
- Try different project: `switch_project("correct-project-name")`
|
|
144
|
+
|
|
145
|
+
If the project should exist but isn't listed, send a message to support@basicmachines.co.
|
|
146
|
+
""").strip()
|
|
119
147
|
|
|
120
148
|
|
|
121
149
|
@mcp.tool()
|
|
@@ -139,7 +167,11 @@ async def get_current_project(ctx: Context | None = None) -> str:
|
|
|
139
167
|
result = f"Current project: {current_project}\n\n"
|
|
140
168
|
|
|
141
169
|
# get project stats
|
|
142
|
-
response = await call_get(
|
|
170
|
+
response = await call_get(
|
|
171
|
+
client,
|
|
172
|
+
f"{project_config.project_url}/project/info",
|
|
173
|
+
params={"project_name": current_project},
|
|
174
|
+
)
|
|
143
175
|
project_info = ProjectInfoResponse.model_validate(response.json())
|
|
144
176
|
|
|
145
177
|
result += f"• {project_info.statistics.total_entities} entities\n"
|
|
@@ -52,6 +52,13 @@ async def read_note(
|
|
|
52
52
|
read_note("Meeting Notes", project="work-project")
|
|
53
53
|
"""
|
|
54
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
|
+
|
|
55
62
|
active_project = get_active_project(project)
|
|
56
63
|
project_url = active_project.project_url
|
|
57
64
|
|
|
@@ -74,7 +81,7 @@ async def read_note(
|
|
|
74
81
|
|
|
75
82
|
# Fallback 1: Try title search via API
|
|
76
83
|
logger.info(f"Search title for: {identifier}")
|
|
77
|
-
title_results = await search_notes(query=identifier, search_type="title", project=project)
|
|
84
|
+
title_results = await search_notes.fn(query=identifier, search_type="title", project=project)
|
|
78
85
|
|
|
79
86
|
if title_results and title_results.results:
|
|
80
87
|
result = title_results.results[0] # Get the first/best match
|
|
@@ -98,7 +105,7 @@ async def read_note(
|
|
|
98
105
|
|
|
99
106
|
# Fallback 2: Text search as a last resort
|
|
100
107
|
logger.info(f"Title search failed, trying text search for: {identifier}")
|
|
101
|
-
text_results = await search_notes(query=identifier, search_type="text", project=project)
|
|
108
|
+
text_results = await search_notes.fn(query=identifier, search_type="text", project=project)
|
|
102
109
|
|
|
103
110
|
# We didn't find a direct match, construct a helpful error message
|
|
104
111
|
if not text_results or not text_results.results:
|
|
@@ -114,7 +121,7 @@ def format_not_found_message(identifier: str) -> str:
|
|
|
114
121
|
return dedent(f"""
|
|
115
122
|
# Note Not Found: "{identifier}"
|
|
116
123
|
|
|
117
|
-
I couldn't find any
|
|
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:
|
|
118
125
|
|
|
119
126
|
## Check Identifier Type
|
|
120
127
|
- If you provided a title, try using the exact permalink instead
|
|
@@ -160,7 +167,7 @@ def format_related_results(identifier: str, results) -> str:
|
|
|
160
167
|
message = dedent(f"""
|
|
161
168
|
# Note Not Found: "{identifier}"
|
|
162
169
|
|
|
163
|
-
I couldn't find an exact match
|
|
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:
|
|
164
171
|
|
|
165
172
|
""")
|
|
166
173
|
|