basic-memory 0.17.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.
- basic_memory/__init__.py +7 -0
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +185 -0
- basic_memory/alembic/migrations.py +24 -0
- basic_memory/alembic/script.py.mako +26 -0
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -0
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
- basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
- basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
- basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
- basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
- basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
- basic_memory/api/__init__.py +5 -0
- basic_memory/api/app.py +131 -0
- basic_memory/api/routers/__init__.py +11 -0
- basic_memory/api/routers/directory_router.py +84 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +318 -0
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +90 -0
- basic_memory/api/routers/project_router.py +448 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +249 -0
- basic_memory/api/routers/search_router.py +36 -0
- basic_memory/api/routers/utils.py +169 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/api/v2/__init__.py +35 -0
- basic_memory/api/v2/routers/__init__.py +21 -0
- basic_memory/api/v2/routers/directory_router.py +93 -0
- basic_memory/api/v2/routers/importer_router.py +182 -0
- basic_memory/api/v2/routers/knowledge_router.py +413 -0
- basic_memory/api/v2/routers/memory_router.py +130 -0
- basic_memory/api/v2/routers/project_router.py +342 -0
- basic_memory/api/v2/routers/prompt_router.py +270 -0
- basic_memory/api/v2/routers/resource_router.py +286 -0
- basic_memory/api/v2/routers/search_router.py +73 -0
- basic_memory/cli/__init__.py +1 -0
- basic_memory/cli/app.py +84 -0
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/__init__.py +18 -0
- basic_memory/cli/commands/cloud/__init__.py +6 -0
- basic_memory/cli/commands/cloud/api_client.py +112 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
- basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
- basic_memory/cli/commands/cloud/core_commands.py +195 -0
- basic_memory/cli/commands/cloud/rclone_commands.py +371 -0
- basic_memory/cli/commands/cloud/rclone_config.py +110 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
- basic_memory/cli/commands/cloud/upload.py +233 -0
- basic_memory/cli/commands/cloud/upload_command.py +124 -0
- basic_memory/cli/commands/command_utils.py +77 -0
- basic_memory/cli/commands/db.py +44 -0
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +84 -0
- basic_memory/cli/commands/import_claude_conversations.py +87 -0
- basic_memory/cli/commands/import_claude_projects.py +86 -0
- basic_memory/cli/commands/import_memory_json.py +87 -0
- basic_memory/cli/commands/mcp.py +76 -0
- basic_memory/cli/commands/project.py +889 -0
- basic_memory/cli/commands/status.py +174 -0
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/main.py +28 -0
- basic_memory/config.py +616 -0
- basic_memory/db.py +394 -0
- basic_memory/deps.py +705 -0
- basic_memory/file_utils.py +478 -0
- basic_memory/ignore_utils.py +297 -0
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +232 -0
- basic_memory/importers/claude_conversations_importer.py +180 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +108 -0
- basic_memory/importers/utils.py +61 -0
- basic_memory/markdown/__init__.py +21 -0
- basic_memory/markdown/entity_parser.py +279 -0
- basic_memory/markdown/markdown_processor.py +160 -0
- basic_memory/markdown/plugins.py +242 -0
- basic_memory/markdown/schemas.py +70 -0
- basic_memory/markdown/utils.py +117 -0
- basic_memory/mcp/__init__.py +1 -0
- basic_memory/mcp/async_client.py +139 -0
- basic_memory/mcp/project_context.py +141 -0
- basic_memory/mcp/prompts/__init__.py +19 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
- basic_memory/mcp/prompts/continue_conversation.py +62 -0
- basic_memory/mcp/prompts/recent_activity.py +188 -0
- basic_memory/mcp/prompts/search.py +57 -0
- basic_memory/mcp/prompts/utils.py +162 -0
- basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
- basic_memory/mcp/resources/project_info.py +71 -0
- basic_memory/mcp/server.py +81 -0
- basic_memory/mcp/tools/__init__.py +48 -0
- basic_memory/mcp/tools/build_context.py +120 -0
- basic_memory/mcp/tools/canvas.py +152 -0
- basic_memory/mcp/tools/chatgpt_tools.py +190 -0
- basic_memory/mcp/tools/delete_note.py +242 -0
- basic_memory/mcp/tools/edit_note.py +324 -0
- basic_memory/mcp/tools/list_directory.py +168 -0
- basic_memory/mcp/tools/move_note.py +551 -0
- basic_memory/mcp/tools/project_management.py +201 -0
- basic_memory/mcp/tools/read_content.py +281 -0
- basic_memory/mcp/tools/read_note.py +267 -0
- basic_memory/mcp/tools/recent_activity.py +534 -0
- basic_memory/mcp/tools/search.py +385 -0
- basic_memory/mcp/tools/utils.py +540 -0
- basic_memory/mcp/tools/view_note.py +78 -0
- basic_memory/mcp/tools/write_note.py +230 -0
- basic_memory/models/__init__.py +15 -0
- basic_memory/models/base.py +10 -0
- basic_memory/models/knowledge.py +226 -0
- basic_memory/models/project.py +87 -0
- basic_memory/models/search.py +85 -0
- basic_memory/repository/__init__.py +11 -0
- basic_memory/repository/entity_repository.py +503 -0
- basic_memory/repository/observation_repository.py +73 -0
- basic_memory/repository/postgres_search_repository.py +379 -0
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +128 -0
- basic_memory/repository/relation_repository.py +146 -0
- basic_memory/repository/repository.py +385 -0
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +94 -0
- basic_memory/repository/search_repository_base.py +241 -0
- basic_memory/repository/sqlite_search_repository.py +439 -0
- basic_memory/schemas/__init__.py +86 -0
- basic_memory/schemas/base.py +297 -0
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/delete.py +37 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +285 -0
- basic_memory/schemas/project_info.py +212 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +112 -0
- basic_memory/schemas/response.py +229 -0
- basic_memory/schemas/search.py +117 -0
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/schemas/v2/__init__.py +27 -0
- basic_memory/schemas/v2/entity.py +129 -0
- basic_memory/schemas/v2/resource.py +46 -0
- basic_memory/services/__init__.py +8 -0
- basic_memory/services/context_service.py +601 -0
- basic_memory/services/directory_service.py +308 -0
- basic_memory/services/entity_service.py +864 -0
- basic_memory/services/exceptions.py +37 -0
- basic_memory/services/file_service.py +541 -0
- basic_memory/services/initialization.py +216 -0
- basic_memory/services/link_resolver.py +121 -0
- basic_memory/services/project_service.py +880 -0
- basic_memory/services/search_service.py +404 -0
- basic_memory/services/service.py +15 -0
- basic_memory/sync/__init__.py +6 -0
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/sync_service.py +1259 -0
- basic_memory/sync/watch_service.py +510 -0
- basic_memory/telemetry.py +249 -0
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +468 -0
- basic_memory-0.17.1.dist-info/METADATA +617 -0
- basic_memory-0.17.1.dist-info/RECORD +171 -0
- basic_memory-0.17.1.dist-info/WHEEL +4 -0
- basic_memory-0.17.1.dist-info/entry_points.txt +3 -0
- basic_memory-0.17.1.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"""Move note tool for Basic Memory MCP server."""
|
|
2
|
+
|
|
3
|
+
from textwrap import dedent
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
from fastmcp import Context
|
|
8
|
+
|
|
9
|
+
from basic_memory.mcp.async_client import get_client
|
|
10
|
+
from basic_memory.mcp.server import mcp
|
|
11
|
+
from basic_memory.mcp.tools.utils import call_get, call_put, resolve_entity_id
|
|
12
|
+
from basic_memory.mcp.project_context import get_active_project
|
|
13
|
+
from basic_memory.schemas import EntityResponse
|
|
14
|
+
from basic_memory.schemas.project_info import ProjectList
|
|
15
|
+
from basic_memory.telemetry import track_mcp_tool
|
|
16
|
+
from basic_memory.utils import validate_project_path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def _detect_cross_project_move_attempt(
|
|
20
|
+
client, identifier: str, destination_path: str, current_project: str
|
|
21
|
+
) -> Optional[str]:
|
|
22
|
+
"""Detect potential cross-project move attempts and return guidance.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
client: The AsyncClient instance
|
|
26
|
+
identifier: The note identifier being moved
|
|
27
|
+
destination_path: The destination path
|
|
28
|
+
current_project: The current active project
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Error message with guidance if cross-project move is detected, None otherwise
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
# Get list of all available projects to check against
|
|
35
|
+
response = await call_get(client, "/projects/projects")
|
|
36
|
+
project_list = ProjectList.model_validate(response.json())
|
|
37
|
+
project_names = [p.name.lower() for p in project_list.projects]
|
|
38
|
+
|
|
39
|
+
# Check if destination path contains any project names
|
|
40
|
+
dest_lower = destination_path.lower()
|
|
41
|
+
path_parts = dest_lower.split("/")
|
|
42
|
+
|
|
43
|
+
# Look for project names in the destination path
|
|
44
|
+
for part in path_parts:
|
|
45
|
+
if part in project_names and part != current_project.lower():
|
|
46
|
+
# Found a different project name in the path
|
|
47
|
+
matching_project = next(
|
|
48
|
+
p.name for p in project_list.projects if p.name.lower() == part
|
|
49
|
+
)
|
|
50
|
+
return _format_cross_project_error_response(
|
|
51
|
+
identifier, destination_path, current_project, matching_project
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# No other cross-project patterns detected
|
|
55
|
+
|
|
56
|
+
except Exception as e:
|
|
57
|
+
# If we can't detect, don't interfere with normal error handling
|
|
58
|
+
logger.debug(f"Could not check for cross-project move: {e}")
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _format_cross_project_error_response(
|
|
65
|
+
identifier: str, destination_path: str, current_project: str, target_project: str
|
|
66
|
+
) -> str:
|
|
67
|
+
"""Format error response for detected cross-project move attempts."""
|
|
68
|
+
return dedent(f"""
|
|
69
|
+
# Move Failed - Cross-Project Move Not Supported
|
|
70
|
+
|
|
71
|
+
Cannot move '{identifier}' to '{destination_path}' because it appears to reference a different project ('{target_project}').
|
|
72
|
+
|
|
73
|
+
**Current project:** {current_project}
|
|
74
|
+
**Target project:** {target_project}
|
|
75
|
+
|
|
76
|
+
## Cross-project moves are not supported directly
|
|
77
|
+
|
|
78
|
+
Notes can only be moved within the same project. To move content between projects, use this workflow:
|
|
79
|
+
|
|
80
|
+
### Recommended approach:
|
|
81
|
+
```
|
|
82
|
+
# 1. Read the note content from current project
|
|
83
|
+
read_note("{identifier}")
|
|
84
|
+
|
|
85
|
+
# 2. Create the note in the target project
|
|
86
|
+
write_note("Note Title", "content from step 1", "target-folder", project="{target_project}")
|
|
87
|
+
|
|
88
|
+
# 3. Delete the original note if desired
|
|
89
|
+
delete_note("{identifier}", project="{current_project}")
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Alternative: Stay in current project
|
|
94
|
+
If you want to move the note within the **{current_project}** project only:
|
|
95
|
+
```
|
|
96
|
+
move_note("{identifier}", "new-folder/new-name.md")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Available projects:
|
|
100
|
+
Use `list_memory_projects()` to see all available projects.
|
|
101
|
+
""").strip()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _format_potential_cross_project_guidance(
|
|
105
|
+
identifier: str, destination_path: str, current_project: str, available_projects: list[str]
|
|
106
|
+
) -> str:
|
|
107
|
+
"""Format guidance for potentially cross-project moves."""
|
|
108
|
+
other_projects = ", ".join(available_projects[:3]) # Show first 3 projects
|
|
109
|
+
if len(available_projects) > 3:
|
|
110
|
+
other_projects += f" (and {len(available_projects) - 3} others)"
|
|
111
|
+
|
|
112
|
+
return dedent(f"""
|
|
113
|
+
# Move Failed - Check Project Context
|
|
114
|
+
|
|
115
|
+
Cannot move '{identifier}' to '{destination_path}' within the current project '{current_project}'.
|
|
116
|
+
|
|
117
|
+
## If you intended to move within the current project:
|
|
118
|
+
The destination path should be relative to the project root:
|
|
119
|
+
```
|
|
120
|
+
move_note("{identifier}", "folder/filename.md")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## If you intended to move to a different project:
|
|
124
|
+
Cross-project moves require switching projects first. Available projects: {other_projects}
|
|
125
|
+
|
|
126
|
+
### To move to another project:
|
|
127
|
+
```
|
|
128
|
+
# 1. Read the content
|
|
129
|
+
read_note("{identifier}")
|
|
130
|
+
|
|
131
|
+
# 2. Create note in target project
|
|
132
|
+
write_note("Title", "content", "folder", project="target-project-name")
|
|
133
|
+
|
|
134
|
+
# 3. Delete original if desired
|
|
135
|
+
delete_note("{identifier}", project="{current_project}")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### To see all projects:
|
|
139
|
+
```
|
|
140
|
+
list_memory_projects()
|
|
141
|
+
```
|
|
142
|
+
""").strip()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _format_move_error_response(error_message: str, identifier: str, destination_path: str) -> str:
|
|
146
|
+
"""Format helpful error responses for move failures that guide users to successful moves."""
|
|
147
|
+
|
|
148
|
+
# Note not found errors
|
|
149
|
+
if "entity not found" in error_message.lower() or "not found" in error_message.lower():
|
|
150
|
+
search_term = identifier.split("/")[-1] if "/" in identifier else identifier
|
|
151
|
+
title_format = (
|
|
152
|
+
identifier.split("/")[-1].replace("-", " ").title() if "/" in identifier else identifier
|
|
153
|
+
)
|
|
154
|
+
permalink_format = identifier.lower().replace(" ", "-")
|
|
155
|
+
|
|
156
|
+
return dedent(f"""
|
|
157
|
+
# Move Failed - Note Not Found
|
|
158
|
+
|
|
159
|
+
The note '{identifier}' could not be found for moving. Move operations require an exact match (no fuzzy matching).
|
|
160
|
+
|
|
161
|
+
## Suggestions to try:
|
|
162
|
+
1. **Search for the note first**: Use `search_notes("{search_term}")` to find it with exact identifiers
|
|
163
|
+
2. **Try different exact identifier formats**:
|
|
164
|
+
- If you used a permalink like "folder/note-title", try the exact title: "{title_format}"
|
|
165
|
+
- If you used a title, try the exact permalink format: "{permalink_format}"
|
|
166
|
+
- Use `read_note()` first to verify the note exists and get the exact identifier
|
|
167
|
+
|
|
168
|
+
3. **List available notes**: Use `list_directory("/")` to see what notes exist in the current project
|
|
169
|
+
4. **List available notes**: Use `list_directory("/")` to see what notes exist
|
|
170
|
+
|
|
171
|
+
## Before trying again:
|
|
172
|
+
```
|
|
173
|
+
# First, verify the note exists:
|
|
174
|
+
search_notes("{identifier}")
|
|
175
|
+
|
|
176
|
+
# Then use the exact identifier from search results:
|
|
177
|
+
move_note("correct-identifier-here", "{destination_path}")
|
|
178
|
+
```
|
|
179
|
+
""").strip()
|
|
180
|
+
|
|
181
|
+
# Destination already exists errors
|
|
182
|
+
if "already exists" in error_message.lower() or "file exists" in error_message.lower():
|
|
183
|
+
return f"""# Move Failed - Destination Already Exists
|
|
184
|
+
|
|
185
|
+
Cannot move '{identifier}' to '{destination_path}' because a file already exists at that location.
|
|
186
|
+
|
|
187
|
+
## How to resolve:
|
|
188
|
+
1. **Choose a different destination**: Try a different filename or folder
|
|
189
|
+
- Add timestamp: `{destination_path.rsplit(".", 1)[0] if "." in destination_path else destination_path}-backup.md`
|
|
190
|
+
- Use different folder: `archive/{destination_path}` or `backup/{destination_path}`
|
|
191
|
+
|
|
192
|
+
2. **Check the existing file**: Use `read_note("{destination_path}")` to see what's already there
|
|
193
|
+
3. **Remove or rename existing**: If safe to do so, move the existing file first
|
|
194
|
+
|
|
195
|
+
## Try these alternatives:
|
|
196
|
+
```
|
|
197
|
+
# Option 1: Add timestamp to make unique
|
|
198
|
+
move_note("{identifier}", "{destination_path.rsplit(".", 1)[0] if "." in destination_path else destination_path}-backup.md")
|
|
199
|
+
|
|
200
|
+
# Option 2: Use archive folder
|
|
201
|
+
move_note("{identifier}", "archive/{destination_path}")
|
|
202
|
+
|
|
203
|
+
# Option 3: Check what's at destination first
|
|
204
|
+
read_note("{destination_path}")
|
|
205
|
+
```"""
|
|
206
|
+
|
|
207
|
+
# Invalid path errors
|
|
208
|
+
if "invalid" in error_message.lower() and "path" in error_message.lower():
|
|
209
|
+
return f"""# Move Failed - Invalid Destination Path
|
|
210
|
+
|
|
211
|
+
The destination path '{destination_path}' is not valid: {error_message}
|
|
212
|
+
|
|
213
|
+
## Path requirements:
|
|
214
|
+
1. **Relative paths only**: Don't start with `/` (use `notes/file.md` not `/notes/file.md`)
|
|
215
|
+
2. **Include file extension**: Add `.md` for markdown files
|
|
216
|
+
3. **Use forward slashes**: For folder separators (`folder/subfolder/file.md`)
|
|
217
|
+
4. **No special characters**: Avoid `\\`, `:`, `*`, `?`, `"`, `<`, `>`, `|`
|
|
218
|
+
|
|
219
|
+
## Valid path examples:
|
|
220
|
+
- `notes/my-note.md`
|
|
221
|
+
- `projects/2025/meeting-notes.md`
|
|
222
|
+
- `archive/old-projects/legacy-note.md`
|
|
223
|
+
|
|
224
|
+
## Try again with:
|
|
225
|
+
```
|
|
226
|
+
move_note("{identifier}", "notes/{destination_path.split("/")[-1] if "/" in destination_path else destination_path}")
|
|
227
|
+
```"""
|
|
228
|
+
|
|
229
|
+
# Permission/access errors
|
|
230
|
+
if (
|
|
231
|
+
"permission" in error_message.lower()
|
|
232
|
+
or "access" in error_message.lower()
|
|
233
|
+
or "forbidden" in error_message.lower()
|
|
234
|
+
):
|
|
235
|
+
return f"""# Move Failed - Permission Error
|
|
236
|
+
|
|
237
|
+
You don't have permission to move '{identifier}': {error_message}
|
|
238
|
+
|
|
239
|
+
## How to resolve:
|
|
240
|
+
1. **Check file permissions**: Ensure you have write access to both source and destination
|
|
241
|
+
2. **Verify project access**: Make sure you have edit permissions for this project
|
|
242
|
+
3. **Check file locks**: The file might be open in another application
|
|
243
|
+
|
|
244
|
+
## Alternative actions:
|
|
245
|
+
- List available projects: `list_memory_projects()`
|
|
246
|
+
- Try copying content instead: `read_note("{identifier}", project="project-name")` then `write_note()` to new location"""
|
|
247
|
+
|
|
248
|
+
# Source file not found errors
|
|
249
|
+
if "source" in error_message.lower() and (
|
|
250
|
+
"not found" in error_message.lower() or "missing" in error_message.lower()
|
|
251
|
+
):
|
|
252
|
+
return f"""# Move Failed - Source File Missing
|
|
253
|
+
|
|
254
|
+
The source file for '{identifier}' was not found on disk: {error_message}
|
|
255
|
+
|
|
256
|
+
This usually means the database and filesystem are out of sync.
|
|
257
|
+
|
|
258
|
+
## How to resolve:
|
|
259
|
+
1. **Check if note exists in database**: `read_note("{identifier}")`
|
|
260
|
+
2. **Run sync operation**: The file might need to be re-synced
|
|
261
|
+
3. **Recreate the file**: If data exists in database, recreate the physical file
|
|
262
|
+
|
|
263
|
+
## Troubleshooting steps:
|
|
264
|
+
```
|
|
265
|
+
# Check if note exists in Basic Memory
|
|
266
|
+
read_note("{identifier}")
|
|
267
|
+
|
|
268
|
+
# If it exists, the file is missing on disk - send a message to support@basicmachines.co
|
|
269
|
+
# If it doesn't exist, use search to find the correct identifier
|
|
270
|
+
search_notes("{identifier}")
|
|
271
|
+
```"""
|
|
272
|
+
|
|
273
|
+
# Server/filesystem errors
|
|
274
|
+
if (
|
|
275
|
+
"server error" in error_message.lower()
|
|
276
|
+
or "filesystem" in error_message.lower()
|
|
277
|
+
or "disk" in error_message.lower()
|
|
278
|
+
):
|
|
279
|
+
return f"""# Move Failed - System Error
|
|
280
|
+
|
|
281
|
+
A system error occurred while moving '{identifier}': {error_message}
|
|
282
|
+
|
|
283
|
+
## Immediate steps:
|
|
284
|
+
1. **Try again**: The error might be temporary
|
|
285
|
+
2. **Check disk space**: Ensure adequate storage is available
|
|
286
|
+
3. **Verify filesystem permissions**: Check if the destination directory is writable
|
|
287
|
+
|
|
288
|
+
## Alternative approaches:
|
|
289
|
+
- Copy content to new location: Use `read_note("{identifier}")` then `write_note()`
|
|
290
|
+
- Use a different destination folder that you know works
|
|
291
|
+
- Send a message to support@basicmachines.co if the problem persists
|
|
292
|
+
|
|
293
|
+
## Backup approach:
|
|
294
|
+
```
|
|
295
|
+
# Read current content
|
|
296
|
+
content = read_note("{identifier}")
|
|
297
|
+
|
|
298
|
+
# Create new note at desired location
|
|
299
|
+
write_note("New Note Title", content, "{destination_path.split("/")[0] if "/" in destination_path else "notes"}")
|
|
300
|
+
|
|
301
|
+
# Then delete original if successful
|
|
302
|
+
delete_note("{identifier}")
|
|
303
|
+
```"""
|
|
304
|
+
|
|
305
|
+
# Generic fallback
|
|
306
|
+
return f"""# Move Failed
|
|
307
|
+
|
|
308
|
+
Error moving '{identifier}' to '{destination_path}': {error_message}
|
|
309
|
+
|
|
310
|
+
## General troubleshooting:
|
|
311
|
+
1. **Verify the note exists**: `read_note("{identifier}")` or `search_notes("{identifier}")`
|
|
312
|
+
2. **Check destination path**: Ensure it's a valid relative path with `.md` extension
|
|
313
|
+
3. **Verify permissions**: Make sure you can edit files in this project
|
|
314
|
+
4. **Try a simpler path**: Use a basic folder structure like `notes/filename.md`
|
|
315
|
+
|
|
316
|
+
## Step-by-step approach:
|
|
317
|
+
```
|
|
318
|
+
# 1. Confirm note exists
|
|
319
|
+
read_note("{identifier}")
|
|
320
|
+
|
|
321
|
+
# 2. Try a simple destination first
|
|
322
|
+
move_note("{identifier}", "notes/{destination_path.split("/")[-1] if "/" in destination_path else destination_path}")
|
|
323
|
+
|
|
324
|
+
# 3. If that works, then try your original destination
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Alternative approach:
|
|
328
|
+
If moving continues to fail, you can copy the content manually:
|
|
329
|
+
```
|
|
330
|
+
# Read current content
|
|
331
|
+
content = read_note("{identifier}")
|
|
332
|
+
|
|
333
|
+
# Create new note
|
|
334
|
+
write_note("Title", content, "target-folder")
|
|
335
|
+
|
|
336
|
+
# Delete original once confirmed
|
|
337
|
+
delete_note("{identifier}")
|
|
338
|
+
```"""
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@mcp.tool(
|
|
342
|
+
description="Move a note to a new location, updating database and maintaining links.",
|
|
343
|
+
)
|
|
344
|
+
async def move_note(
|
|
345
|
+
identifier: str,
|
|
346
|
+
destination_path: str,
|
|
347
|
+
project: Optional[str] = None,
|
|
348
|
+
context: Context | None = None,
|
|
349
|
+
) -> str:
|
|
350
|
+
"""Move a note to a new file location within the same project.
|
|
351
|
+
|
|
352
|
+
Moves a note from one location to another within the project, updating all
|
|
353
|
+
database references and maintaining semantic content. Uses stateless architecture -
|
|
354
|
+
project parameter optional with server resolution.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
identifier: Exact entity identifier (title, permalink, or memory:// URL).
|
|
358
|
+
Must be an exact match - fuzzy matching is not supported for move operations.
|
|
359
|
+
Use search_notes() or read_note() first to find the correct identifier if uncertain.
|
|
360
|
+
destination_path: New path relative to project root (e.g., "work/meetings/2025-05-26.md")
|
|
361
|
+
project: Project name to move within. Optional - server will resolve using hierarchy.
|
|
362
|
+
If unknown, use list_memory_projects() to discover available projects.
|
|
363
|
+
context: Optional FastMCP context for performance caching.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Success message with move details and project information.
|
|
367
|
+
|
|
368
|
+
Examples:
|
|
369
|
+
# Move to new folder (exact title match)
|
|
370
|
+
move_note("My Note", "work/notes/my-note.md")
|
|
371
|
+
|
|
372
|
+
# Move by exact permalink
|
|
373
|
+
move_note("my-note-permalink", "archive/old-notes/my-note.md")
|
|
374
|
+
|
|
375
|
+
# Move with complex path structure
|
|
376
|
+
move_note("experiments/ml-results", "archive/2025/ml-experiments.md")
|
|
377
|
+
|
|
378
|
+
# Explicit project specification
|
|
379
|
+
move_note("My Note", "work/notes/my-note.md", project="work-project")
|
|
380
|
+
|
|
381
|
+
# If uncertain about identifier, search first:
|
|
382
|
+
# search_notes("my note") # Find available notes
|
|
383
|
+
# move_note("docs/my-note-2025", "archive/my-note.md") # Use exact result
|
|
384
|
+
|
|
385
|
+
Raises:
|
|
386
|
+
ToolError: If project doesn't exist, identifier is not found, or destination_path is invalid
|
|
387
|
+
|
|
388
|
+
Note:
|
|
389
|
+
This operation moves notes within the specified project only. Moving notes
|
|
390
|
+
between different projects is not currently supported.
|
|
391
|
+
|
|
392
|
+
The move operation:
|
|
393
|
+
- Updates the entity's file_path in the database
|
|
394
|
+
- Moves the physical file on the filesystem
|
|
395
|
+
- Optionally updates permalinks if configured
|
|
396
|
+
- Re-indexes the entity for search
|
|
397
|
+
- Maintains all observations and relations
|
|
398
|
+
"""
|
|
399
|
+
track_mcp_tool("move_note")
|
|
400
|
+
async with get_client() as client:
|
|
401
|
+
logger.debug(f"Moving note: {identifier} to {destination_path} in project: {project}")
|
|
402
|
+
|
|
403
|
+
active_project = await get_active_project(client, project, context)
|
|
404
|
+
|
|
405
|
+
# Validate destination path to prevent path traversal attacks
|
|
406
|
+
project_path = active_project.home
|
|
407
|
+
if not validate_project_path(destination_path, project_path):
|
|
408
|
+
logger.warning(
|
|
409
|
+
"Attempted path traversal attack blocked",
|
|
410
|
+
destination_path=destination_path,
|
|
411
|
+
project=active_project.name,
|
|
412
|
+
)
|
|
413
|
+
return f"""# Move Failed - Security Validation Error
|
|
414
|
+
|
|
415
|
+
The destination path '{destination_path}' is not allowed - paths must stay within project boundaries.
|
|
416
|
+
|
|
417
|
+
## Valid path examples:
|
|
418
|
+
- `notes/my-file.md`
|
|
419
|
+
- `projects/2025/meeting-notes.md`
|
|
420
|
+
- `archive/old-notes.md`
|
|
421
|
+
|
|
422
|
+
## Try again with a safe path:
|
|
423
|
+
```
|
|
424
|
+
move_note("{identifier}", "notes/{destination_path.split("/")[-1] if "/" in destination_path else destination_path}")
|
|
425
|
+
```"""
|
|
426
|
+
|
|
427
|
+
# Check for potential cross-project move attempts
|
|
428
|
+
cross_project_error = await _detect_cross_project_move_attempt(
|
|
429
|
+
client, identifier, destination_path, active_project.name
|
|
430
|
+
)
|
|
431
|
+
if cross_project_error:
|
|
432
|
+
logger.info(f"Detected cross-project move attempt: {identifier} -> {destination_path}")
|
|
433
|
+
return cross_project_error
|
|
434
|
+
|
|
435
|
+
# Get the source entity information for extension validation
|
|
436
|
+
source_ext = "md" # Default to .md if we can't determine source extension
|
|
437
|
+
try:
|
|
438
|
+
# Resolve identifier to entity ID
|
|
439
|
+
entity_id = await resolve_entity_id(client, active_project.id, identifier)
|
|
440
|
+
# Fetch source entity information to get the current file extension
|
|
441
|
+
url = f"/v2/projects/{active_project.id}/knowledge/entities/{entity_id}"
|
|
442
|
+
response = await call_get(client, url)
|
|
443
|
+
source_entity = EntityResponse.model_validate(response.json())
|
|
444
|
+
if "." in source_entity.file_path:
|
|
445
|
+
source_ext = source_entity.file_path.split(".")[-1]
|
|
446
|
+
except Exception as e:
|
|
447
|
+
# If we can't fetch the source entity, default to .md extension
|
|
448
|
+
logger.debug(f"Could not fetch source entity for extension check: {e}")
|
|
449
|
+
|
|
450
|
+
# Validate that destination path includes a file extension
|
|
451
|
+
if "." not in destination_path or not destination_path.split(".")[-1]:
|
|
452
|
+
logger.warning(f"Move failed - no file extension provided: {destination_path}")
|
|
453
|
+
return dedent(f"""
|
|
454
|
+
# Move Failed - File Extension Required
|
|
455
|
+
|
|
456
|
+
The destination path '{destination_path}' must include a file extension (e.g., '.md').
|
|
457
|
+
|
|
458
|
+
## Valid examples:
|
|
459
|
+
- `notes/my-note.md`
|
|
460
|
+
- `projects/meeting-2025.txt`
|
|
461
|
+
- `archive/old-program.sh`
|
|
462
|
+
|
|
463
|
+
## Try again with extension:
|
|
464
|
+
```
|
|
465
|
+
move_note("{identifier}", "{destination_path}.{source_ext}")
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
All examples in Basic Memory expect file extensions to be explicitly provided.
|
|
469
|
+
""").strip()
|
|
470
|
+
|
|
471
|
+
# Get the source entity to check its file extension
|
|
472
|
+
try:
|
|
473
|
+
# Resolve identifier to entity ID (might already be cached from above)
|
|
474
|
+
entity_id = await resolve_entity_id(client, active_project.id, identifier)
|
|
475
|
+
# Fetch source entity information
|
|
476
|
+
url = f"/v2/projects/{active_project.id}/knowledge/entities/{entity_id}"
|
|
477
|
+
response = await call_get(client, url)
|
|
478
|
+
source_entity = EntityResponse.model_validate(response.json())
|
|
479
|
+
|
|
480
|
+
# Extract file extensions
|
|
481
|
+
source_ext = (
|
|
482
|
+
source_entity.file_path.split(".")[-1] if "." in source_entity.file_path else ""
|
|
483
|
+
)
|
|
484
|
+
dest_ext = destination_path.split(".")[-1] if "." in destination_path else ""
|
|
485
|
+
|
|
486
|
+
# Check if extensions match
|
|
487
|
+
if source_ext and dest_ext and source_ext.lower() != dest_ext.lower():
|
|
488
|
+
logger.warning(
|
|
489
|
+
f"Move failed - file extension mismatch: source={source_ext}, dest={dest_ext}"
|
|
490
|
+
)
|
|
491
|
+
return dedent(f"""
|
|
492
|
+
# Move Failed - File Extension Mismatch
|
|
493
|
+
|
|
494
|
+
The destination file extension '.{dest_ext}' does not match the source file extension '.{source_ext}'.
|
|
495
|
+
|
|
496
|
+
To preserve file type consistency, the destination must have the same extension as the source.
|
|
497
|
+
|
|
498
|
+
## Source file:
|
|
499
|
+
- Path: `{source_entity.file_path}`
|
|
500
|
+
- Extension: `.{source_ext}`
|
|
501
|
+
|
|
502
|
+
## Try again with matching extension:
|
|
503
|
+
```
|
|
504
|
+
move_note("{identifier}", "{destination_path.rsplit(".", 1)[0]}.{source_ext}")
|
|
505
|
+
```
|
|
506
|
+
""").strip()
|
|
507
|
+
except Exception as e:
|
|
508
|
+
# If we can't fetch the source entity, log it but continue
|
|
509
|
+
# This might happen if the identifier is not yet resolved
|
|
510
|
+
logger.debug(f"Could not fetch source entity for extension check: {e}")
|
|
511
|
+
|
|
512
|
+
try:
|
|
513
|
+
# Resolve identifier to entity ID for the move operation
|
|
514
|
+
entity_id = await resolve_entity_id(client, active_project.id, identifier)
|
|
515
|
+
|
|
516
|
+
# Prepare move request (v2 API only needs destination_path)
|
|
517
|
+
move_data = {
|
|
518
|
+
"destination_path": destination_path,
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
# Call the v2 move API endpoint (PUT method, entity_id in URL)
|
|
522
|
+
url = f"/v2/projects/{active_project.id}/knowledge/entities/{entity_id}/move"
|
|
523
|
+
response = await call_put(client, url, json=move_data)
|
|
524
|
+
result = EntityResponse.model_validate(response.json())
|
|
525
|
+
|
|
526
|
+
# Build success message
|
|
527
|
+
result_lines = [
|
|
528
|
+
"✅ Note moved successfully",
|
|
529
|
+
"",
|
|
530
|
+
f"📁 **{identifier}** → **{result.file_path}**",
|
|
531
|
+
f"🔗 Permalink: {result.permalink}",
|
|
532
|
+
"📊 Database and search index updated",
|
|
533
|
+
"",
|
|
534
|
+
f"<!-- Project: {active_project.name} -->",
|
|
535
|
+
]
|
|
536
|
+
|
|
537
|
+
# Log the operation
|
|
538
|
+
logger.info(
|
|
539
|
+
"Move note completed",
|
|
540
|
+
identifier=identifier,
|
|
541
|
+
destination_path=destination_path,
|
|
542
|
+
project=active_project.name,
|
|
543
|
+
status_code=response.status_code,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
return "\n".join(result_lines)
|
|
547
|
+
|
|
548
|
+
except Exception as e:
|
|
549
|
+
logger.error(f"Move failed for '{identifier}' to '{destination_path}': {e}")
|
|
550
|
+
# Return formatted error message for better user experience
|
|
551
|
+
return _format_move_error_response(str(e), identifier, destination_path)
|