-
Notifications
You must be signed in to change notification settings - Fork 188
Expand file tree
/
Copy pathdelete_note.py
More file actions
406 lines (336 loc) · 16.6 KB
/
delete_note.py
File metadata and controls
406 lines (336 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
from textwrap import dedent
from typing import Optional, Literal
from loguru import logger
from fastmcp import Context
from mcp.server.fastmcp.exceptions import ToolError
from basic_memory.config import ConfigManager
from basic_memory.mcp.project_context import detect_project_from_url_prefix, get_project_client
from basic_memory.mcp.server import mcp
def _format_delete_error_response(project: str, error_message: str, identifier: str) -> str:
"""Format helpful error responses for delete failures that guide users to successful deletions."""
# Note not found errors
if "entity not found" in error_message.lower() or "not found" in error_message.lower():
search_term = identifier.split("/")[-1] if "/" in identifier else identifier
title_format = (
identifier.split("/")[-1].replace("-", " ").title() if "/" in identifier else identifier
)
permalink_format = identifier.lower().replace(" ", "-")
return dedent(f"""
# Delete Failed - Note Not Found
The note '{identifier}' could not be found for deletion in {project}.
## This might mean:
1. **Already deleted**: The note may have been deleted previously
2. **Wrong identifier**: The identifier format might be incorrect
3. **Different project**: The note might be in a different project
## How to verify:
1. **Search for the note**: Use `search_notes("{project}", "{search_term}")` to find it
2. **Try different formats**:
- If you used a permalink like "folder/note-title", try just the title: "{title_format}"
- If you used a title, try the permalink format: "{permalink_format}"
3. **Check if already deleted**: Use `list_directory("/")` to see what notes exist
4. **List notes in project**: Use `list_directory("/")` to see what notes exist in the current project
## If the note actually exists:
```
# First, find the correct identifier:
search_notes("{project}", "{identifier}")
# Then delete using the correct identifier:
delete_note("{project}", "correct-identifier-from-search")
```
## If you want to delete multiple similar notes:
Use search to find all related notes and delete them one by one.
""").strip()
# Permission/access errors
if (
"permission" in error_message.lower()
or "access" in error_message.lower()
or "forbidden" in error_message.lower()
):
return f"""# Delete Failed - Permission Error
You don't have permission to delete '{identifier}': {error_message}
## How to resolve:
1. **Check permissions**: Verify you have delete/write access to this project
2. **File locks**: The note might be open in another application
3. **Project access**: Ensure you're in the correct project with proper permissions
## Alternative actions:
- List available projects: `list_memory_projects()`
- Specify the correct project: `delete_note("{identifier}", project="project-name")`
- Verify note exists first: `read_note("{identifier}", project="project-name")`
## If you have read-only access:
Ask someone with write access to delete the note."""
# Server/filesystem errors
if (
"server error" in error_message.lower()
or "filesystem" in error_message.lower()
or "disk" in error_message.lower()
):
return f"""# Delete Failed - System Error
A system error occurred while deleting '{identifier}': {error_message}
## Immediate steps:
1. **Try again**: The error might be temporary
2. **Check file status**: Verify the file isn't locked or in use
3. **Check disk space**: Ensure the system has adequate storage
## Troubleshooting:
- Verify note exists: `read_note("{project}","{identifier}")`
- Try again in a few moments
## If problem persists:
Send a message to support@basicmachines.co - there may be a filesystem or database issue."""
# Database/sync errors
if "database" in error_message.lower() or "sync" in error_message.lower():
return f"""# Delete Failed - Database Error
A database error occurred while deleting '{identifier}': {error_message}
## This usually means:
1. **Sync conflict**: The file system and database are out of sync
2. **Database lock**: Another operation is accessing the database
3. **Corrupted entry**: The database entry might be corrupted
## Steps to resolve:
1. **Try again**: Wait a moment and retry the deletion
2. **Check note status**: `read_note("{project}","{identifier}")` to see current state
3. **Manual verification**: Use `list_directory()` to see if file still exists
## If the note appears gone but database shows it exists:
Send a message to support@basicmachines.co - a manual database cleanup may be needed."""
# Generic fallback
return f"""# Delete Failed
Error deleting note '{identifier}': {error_message}
## General troubleshooting:
1. **Verify the note exists**: `read_note("{project}", "{identifier}")` or `search_notes("{project}", "{identifier}")`
2. **Check permissions**: Ensure you can edit/delete files in this project
3. **Try again**: The error might be temporary
4. **Check project**: Make sure you're in the correct project
## Step-by-step approach:
```
# 1. Confirm note exists and get correct identifier
search_notes("{project}", "{identifier}")
# 2. Read the note to verify access
read_note("{project}", "correct-identifier-from-search")
# 3. Try deletion with correct identifier
delete_note("{project}", "correct-identifier-from-search")
```
## Alternative approaches:
- Check what notes exist: `list_directory("{project}", "/")`
## Need help?
If the note should be deleted but the operation keeps failing, send a message to support@basicmemory.com."""
@mcp.tool(
description="Delete a note or directory by title, permalink, or path",
annotations={"destructiveHint": True, "openWorldHint": False},
)
async def delete_note(
identifier: str,
is_directory: bool = False,
project: Optional[str] = None,
workspace: Optional[str] = None,
output_format: Literal["text", "json"] = "text",
context: Context | None = None,
) -> bool | str | dict:
"""Delete a note or directory from the knowledge base.
Permanently removes a note or directory from the specified project. For single notes,
they are identified by title or permalink. For directories, use is_directory=True and
provide the directory path. If the note/directory doesn't exist, the operation returns
False without error. If deletion fails, helpful error messages are provided.
Project Resolution:
Server resolves projects in this order: Single Project Mode → project parameter → default project.
If project unknown, use list_memory_projects() or recent_activity() first.
Args:
identifier: For files: note title or permalink to delete.
For directories: the directory path (e.g., "docs", "projects/2025").
Can be a title like "Meeting Notes" or permalink like "notes/meeting-notes"
is_directory: If True, deletes an entire directory and all its contents.
When True, identifier should be a directory path
(without file extensions). Defaults to False.
project: Project name to delete from. Optional - server will resolve using hierarchy.
If unknown, use list_memory_projects() to discover available projects.
output_format: "text" preserves existing behavior (bool/string). "json"
returns machine-readable deletion metadata.
context: Optional FastMCP context for performance caching.
Returns:
True if note was successfully deleted, False if note was not found.
For directories, returns a formatted summary of deleted files.
On errors, returns a formatted string with helpful troubleshooting guidance.
Examples:
# Delete by title
delete_note("Meeting Notes: Project Planning")
# Delete by permalink
delete_note("notes/project-planning")
# Delete with explicit project
delete_note("experiments/ml-model-results", project="research")
# Delete entire directory
delete_note("docs", is_directory=True)
# Delete nested directory
delete_note("projects/2024", is_directory=True)
# Common usage pattern
if delete_note("old-draft"):
print("Note deleted successfully")
else:
print("Note not found or already deleted")
Raises:
HTTPError: If project doesn't exist or is inaccessible
SecurityError: If identifier attempts path traversal
Warning:
This operation is permanent and cannot be undone. The note/directory files
will be removed from the filesystem and all references will be lost.
Note:
If the note is not found, this function provides helpful error messages
with suggestions for finding the correct identifier, including search
commands and alternative formats to try.
"""
# Detect project from memory URL prefix before routing
# Trigger: identifier starts with memory:// and no explicit project was provided
# Why: only gate on memory:// to avoid misrouting plain paths like "research/note"
# where "research" is a directory, not a project name
# Outcome: project is set from the URL prefix, routing goes to the correct project
if project is None and identifier.strip().startswith("memory://"):
detected = detect_project_from_url_prefix(identifier, ConfigManager().config)
if detected:
project = detected
async with get_project_client(project, workspace, context) as (client, active_project):
logger.debug(
f"Deleting {'directory' if is_directory else 'note'}: {identifier} in project: {active_project.name}"
)
# Import here to avoid circular import
from basic_memory.mcp.clients import KnowledgeClient
# Use typed KnowledgeClient for API calls
knowledge_client = KnowledgeClient(client, active_project.external_id)
# Handle directory deletes
if is_directory:
try:
result = await knowledge_client.delete_directory(identifier)
if output_format == "json":
return {
"deleted": result.failed_deletes == 0,
"is_directory": True,
"identifier": identifier,
"total_files": result.total_files,
"successful_deletes": result.successful_deletes,
"failed_deletes": result.failed_deletes,
}
# Build success message for directory delete
result_lines = [
"# Directory Deleted Successfully",
"",
f"**Directory:** `{identifier}`",
"",
"## Summary",
f"- Total files: {result.total_files}",
f"- Successfully deleted: {result.successful_deletes}",
f"- Failed: {result.failed_deletes}",
]
if result.deleted_files:
result_lines.extend(["", "## Deleted Files"])
for file_path in result.deleted_files[:10]: # Show first 10
result_lines.append(f"- `{file_path}`")
if len(result.deleted_files) > 10:
result_lines.append(f"- ... and {len(result.deleted_files) - 10} more")
if result.errors: # pragma: no cover
result_lines.extend(["", "## Errors"])
for error in result.errors[:5]: # Show first 5 errors
result_lines.append(f"- `{error.path}`: {error.error}")
if len(result.errors) > 5:
result_lines.append(f"- ... and {len(result.errors) - 5} more errors")
result_lines.extend(["", f"<!-- Project: {active_project.name} -->"])
logger.info(
f"Directory delete completed: {identifier}, "
f"deleted={result.successful_deletes}, failed={result.failed_deletes}"
)
return "\n".join(result_lines)
except Exception as e: # pragma: no cover
logger.error(f"Directory delete failed for '{identifier}': {e}")
if output_format == "json":
return {
"deleted": False,
"is_directory": True,
"identifier": identifier,
"total_files": 0,
"successful_deletes": 0,
"failed_deletes": 0,
"error": str(e),
}
return f"""# Directory Delete Failed
Error deleting directory '{identifier}': {str(e)}
## Troubleshooting:
1. **Verify the directory exists**: Use `list_directory("{identifier}")` to check
2. **Check for permission issues**: Ensure you have delete access to the project
3. **Try individual deletes**: Delete files one at a time if bulk delete fails
## Alternative approach:
```
# List directory contents first
list_directory("{identifier}")
# Then delete individual files
delete_note("path/to/file.md")
```"""
# Handle single note deletes
note_title = None
note_permalink = None
note_file_path = None
try:
# Resolve identifier to entity ID
entity_id = await knowledge_client.resolve_entity(identifier, strict=True)
if output_format == "json":
entity = await knowledge_client.get_entity(entity_id)
note_title = entity.title
note_permalink = entity.permalink
note_file_path = entity.file_path
except ToolError as e:
# If entity not found, return False (note doesn't exist)
if "Entity not found" in str(e) or "not found" in str(e).lower():
logger.warning(f"Note not found for deletion: {identifier}")
if output_format == "json":
return {
"deleted": False,
"title": None,
"permalink": None,
"file_path": None,
}
return False
# For other resolution errors, return formatted error message
logger.error( # pragma: no cover
f"Delete failed for '{identifier}': {e}, project: {active_project.name}"
)
if output_format == "json":
return {
"deleted": False,
"title": None,
"permalink": None,
"file_path": None,
"error": str(e),
}
return _format_delete_error_response( # pragma: no cover
active_project.name, str(e), identifier
)
try:
# Call the DELETE endpoint
result = await knowledge_client.delete_entity(entity_id)
if result.deleted:
logger.info(
f"Successfully deleted note: {identifier} in project: {active_project.name}"
)
if output_format == "json":
return {
"deleted": True,
"title": note_title,
"permalink": note_permalink,
"file_path": note_file_path,
}
return True
else:
logger.warning( # pragma: no cover
f"Delete operation completed but note was not deleted: {identifier}"
)
if output_format == "json":
return {
"deleted": False,
"title": note_title,
"permalink": note_permalink,
"file_path": note_file_path,
}
return False # pragma: no cover
except Exception as e: # pragma: no cover
logger.error(f"Delete failed for '{identifier}': {e}, project: {active_project.name}")
if output_format == "json":
return {
"deleted": False,
"title": note_title,
"permalink": note_permalink,
"file_path": note_file_path,
"error": str(e),
}
# Return formatted error message for better user experience
return _format_delete_error_response(active_project.name, str(e), identifier)