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