basic-memory 0.14.3__py3-none-any.whl → 0.15.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of basic-memory might be problematic. Click here for more details.

Files changed (90) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  3. basic_memory/api/app.py +10 -4
  4. basic_memory/api/routers/knowledge_router.py +25 -8
  5. basic_memory/api/routers/project_router.py +99 -4
  6. basic_memory/api/routers/resource_router.py +3 -3
  7. basic_memory/cli/app.py +9 -28
  8. basic_memory/cli/auth.py +277 -0
  9. basic_memory/cli/commands/cloud/__init__.py +5 -0
  10. basic_memory/cli/commands/cloud/api_client.py +112 -0
  11. basic_memory/cli/commands/cloud/bisync_commands.py +818 -0
  12. basic_memory/cli/commands/cloud/core_commands.py +288 -0
  13. basic_memory/cli/commands/cloud/mount_commands.py +295 -0
  14. basic_memory/cli/commands/cloud/rclone_config.py +288 -0
  15. basic_memory/cli/commands/cloud/rclone_installer.py +198 -0
  16. basic_memory/cli/commands/command_utils.py +60 -0
  17. basic_memory/cli/commands/import_memory_json.py +0 -4
  18. basic_memory/cli/commands/mcp.py +16 -4
  19. basic_memory/cli/commands/project.py +141 -145
  20. basic_memory/cli/commands/status.py +34 -22
  21. basic_memory/cli/commands/sync.py +45 -228
  22. basic_memory/cli/commands/tool.py +87 -16
  23. basic_memory/cli/main.py +1 -0
  24. basic_memory/config.py +96 -20
  25. basic_memory/db.py +104 -3
  26. basic_memory/deps.py +20 -3
  27. basic_memory/file_utils.py +89 -0
  28. basic_memory/ignore_utils.py +295 -0
  29. basic_memory/importers/chatgpt_importer.py +1 -1
  30. basic_memory/importers/utils.py +2 -2
  31. basic_memory/markdown/entity_parser.py +2 -2
  32. basic_memory/markdown/markdown_processor.py +2 -2
  33. basic_memory/markdown/plugins.py +39 -21
  34. basic_memory/markdown/utils.py +1 -1
  35. basic_memory/mcp/async_client.py +22 -10
  36. basic_memory/mcp/project_context.py +141 -0
  37. basic_memory/mcp/prompts/ai_assistant_guide.py +49 -4
  38. basic_memory/mcp/prompts/continue_conversation.py +1 -1
  39. basic_memory/mcp/prompts/recent_activity.py +116 -32
  40. basic_memory/mcp/prompts/search.py +1 -1
  41. basic_memory/mcp/prompts/utils.py +11 -4
  42. basic_memory/mcp/resources/ai_assistant_guide.md +179 -41
  43. basic_memory/mcp/resources/project_info.py +20 -6
  44. basic_memory/mcp/server.py +0 -37
  45. basic_memory/mcp/tools/__init__.py +5 -6
  46. basic_memory/mcp/tools/build_context.py +39 -19
  47. basic_memory/mcp/tools/canvas.py +19 -8
  48. basic_memory/mcp/tools/chatgpt_tools.py +178 -0
  49. basic_memory/mcp/tools/delete_note.py +67 -34
  50. basic_memory/mcp/tools/edit_note.py +55 -39
  51. basic_memory/mcp/tools/headers.py +44 -0
  52. basic_memory/mcp/tools/list_directory.py +18 -8
  53. basic_memory/mcp/tools/move_note.py +119 -41
  54. basic_memory/mcp/tools/project_management.py +77 -229
  55. basic_memory/mcp/tools/read_content.py +28 -12
  56. basic_memory/mcp/tools/read_note.py +97 -57
  57. basic_memory/mcp/tools/recent_activity.py +441 -42
  58. basic_memory/mcp/tools/search.py +82 -70
  59. basic_memory/mcp/tools/sync_status.py +5 -4
  60. basic_memory/mcp/tools/utils.py +19 -0
  61. basic_memory/mcp/tools/view_note.py +31 -6
  62. basic_memory/mcp/tools/write_note.py +65 -14
  63. basic_memory/models/knowledge.py +19 -2
  64. basic_memory/models/project.py +6 -2
  65. basic_memory/repository/entity_repository.py +31 -84
  66. basic_memory/repository/project_repository.py +1 -1
  67. basic_memory/repository/relation_repository.py +13 -0
  68. basic_memory/repository/repository.py +2 -2
  69. basic_memory/repository/search_repository.py +9 -3
  70. basic_memory/schemas/__init__.py +6 -0
  71. basic_memory/schemas/base.py +70 -12
  72. basic_memory/schemas/cloud.py +46 -0
  73. basic_memory/schemas/memory.py +99 -18
  74. basic_memory/schemas/project_info.py +9 -10
  75. basic_memory/schemas/sync_report.py +48 -0
  76. basic_memory/services/context_service.py +35 -11
  77. basic_memory/services/directory_service.py +7 -0
  78. basic_memory/services/entity_service.py +82 -52
  79. basic_memory/services/initialization.py +30 -11
  80. basic_memory/services/project_service.py +23 -33
  81. basic_memory/sync/sync_service.py +148 -24
  82. basic_memory/sync/watch_service.py +128 -44
  83. basic_memory/utils.py +181 -109
  84. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/METADATA +26 -96
  85. basic_memory-0.15.0.dist-info/RECORD +147 -0
  86. basic_memory/mcp/project_session.py +0 -120
  87. basic_memory-0.14.3.dist-info/RECORD +0 -132
  88. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/WHEEL +0 -0
  89. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/entry_points.txt +0 -0
  90. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,11 +7,12 @@ import json
7
7
  from typing import Dict, List, Any, Optional
8
8
 
9
9
  from loguru import logger
10
+ from fastmcp import Context
10
11
 
11
12
  from basic_memory.mcp.async_client import client
13
+ from basic_memory.mcp.project_context import get_active_project
12
14
  from basic_memory.mcp.server import mcp
13
15
  from basic_memory.mcp.tools.utils import call_put
14
- from basic_memory.mcp.project_session import get_active_project
15
16
 
16
17
 
17
18
  @mcp.tool(
@@ -23,21 +24,28 @@ async def canvas(
23
24
  title: str,
24
25
  folder: str,
25
26
  project: Optional[str] = None,
27
+ context: Context | None = None,
26
28
  ) -> str:
27
29
  """Create an Obsidian canvas file with the provided nodes and edges.
28
30
 
29
31
  This tool creates a .canvas file compatible with Obsidian's Canvas feature,
30
32
  allowing visualization of relationships between concepts or documents.
31
33
 
34
+ Project Resolution:
35
+ Server resolves projects in this order: Single Project Mode → project parameter → default project.
36
+ If project unknown, use list_memory_projects() or recent_activity() first.
37
+
32
38
  For the full JSON Canvas 1.0 specification, see the 'spec://canvas' resource.
33
39
 
34
40
  Args:
41
+ project: Project name to create canvas in. Optional - server will resolve using hierarchy.
42
+ If unknown, use list_memory_projects() to discover available projects.
35
43
  nodes: List of node objects following JSON Canvas 1.0 spec
36
44
  edges: List of edge objects following JSON Canvas 1.0 spec
37
45
  title: The title of the canvas (will be saved as title.canvas)
38
46
  folder: Folder path relative to project root where the canvas should be saved.
39
47
  Use forward slashes (/) as separators. Examples: "diagrams", "projects/2025", "visual/maps"
40
- project: Optional project name to create canvas in. If not provided, uses current active project.
48
+ context: Optional FastMCP context for performance caching.
41
49
 
42
50
  Returns:
43
51
  A summary of the created canvas file
@@ -77,13 +85,16 @@ async def canvas(
77
85
  ```
78
86
 
79
87
  Examples:
80
- # Create canvas in current project
81
- canvas(nodes=[...], edges=[...], title="My Canvas", folder="diagrams")
88
+ # Create canvas in project
89
+ canvas("my-project", nodes=[...], edges=[...], title="My Canvas", folder="diagrams")
90
+
91
+ # Create canvas in work project
92
+ canvas("work-project", nodes=[...], edges=[...], title="Process Flow", folder="visual/maps")
82
93
 
83
- # Create canvas in specific project
84
- canvas(nodes=[...], edges=[...], title="My Canvas", folder="diagrams", project="work-project")
94
+ Raises:
95
+ ToolError: If project doesn't exist or folder path is invalid
85
96
  """
86
- active_project = get_active_project(project)
97
+ active_project = await get_active_project(client, project, context)
87
98
  project_url = active_project.project_url
88
99
 
89
100
  # Ensure path has .canvas extension
@@ -97,7 +108,7 @@ async def canvas(
97
108
  canvas_json = json.dumps(canvas_data, indent=2)
98
109
 
99
110
  # Write the file using the resource API
100
- logger.info(f"Creating canvas file: {file_path}")
111
+ logger.info(f"Creating canvas file: {file_path} in project {project}")
101
112
  response = await call_put(client, f"{project_url}/resource/{file_path}", json=canvas_json)
102
113
 
103
114
  # Parse response
@@ -0,0 +1,178 @@
1
+ """ChatGPT-compatible MCP tools for Basic Memory.
2
+
3
+ These adapters expose Basic Memory's search/fetch functionality using the exact
4
+ tool names and response structure OpenAI's MCP clients expect: each call returns
5
+ a list containing a single `{"type": "text", "text": "{...json...}"}` item.
6
+ """
7
+
8
+ import json
9
+ from typing import Any, Dict, List, Optional
10
+ from loguru import logger
11
+ from fastmcp import Context
12
+
13
+ from basic_memory.mcp.server import mcp
14
+ from basic_memory.mcp.tools.search import search_notes
15
+ from basic_memory.mcp.tools.read_note import read_note
16
+ from basic_memory.schemas.search import SearchResponse
17
+
18
+
19
+ def _format_search_results_for_chatgpt(results: SearchResponse) -> List[Dict[str, Any]]:
20
+ """Format search results according to ChatGPT's expected schema.
21
+
22
+ Returns a list of result objects with id, title, and url fields.
23
+ """
24
+ formatted_results = []
25
+
26
+ for result in results.results:
27
+ formatted_result = {
28
+ "id": result.permalink or f"doc-{len(formatted_results)}",
29
+ "title": result.title if result.title and result.title.strip() else "Untitled",
30
+ "url": result.permalink or "",
31
+ }
32
+ formatted_results.append(formatted_result)
33
+
34
+ return formatted_results
35
+
36
+
37
+ def _format_document_for_chatgpt(
38
+ content: str, identifier: str, title: Optional[str] = None
39
+ ) -> Dict[str, Any]:
40
+ """Format document content according to ChatGPT's expected schema.
41
+
42
+ Returns a document object with id, title, text, url, and metadata fields.
43
+ """
44
+ # Extract title from markdown content if not provided
45
+ if not title and isinstance(content, str):
46
+ lines = content.split("\n")
47
+ if lines and lines[0].startswith("# "):
48
+ title = lines[0][2:].strip()
49
+ else:
50
+ title = identifier.split("/")[-1].replace("-", " ").title()
51
+
52
+ # Ensure title is never None
53
+ if not title:
54
+ title = "Untitled Document"
55
+
56
+ # Handle error cases
57
+ if isinstance(content, str) and content.startswith("# Note Not Found"):
58
+ return {
59
+ "id": identifier,
60
+ "title": title or "Document Not Found",
61
+ "text": content,
62
+ "url": identifier,
63
+ "metadata": {"error": "Document not found"},
64
+ }
65
+
66
+ return {
67
+ "id": identifier,
68
+ "title": title or "Untitled Document",
69
+ "text": content,
70
+ "url": identifier,
71
+ "metadata": {"format": "markdown"},
72
+ }
73
+
74
+
75
+ @mcp.tool(description="Search for content across the knowledge base")
76
+ async def search(
77
+ query: str,
78
+ context: Context | None = None,
79
+ ) -> List[Dict[str, Any]]:
80
+ """ChatGPT/OpenAI MCP search adapter returning a single text content item.
81
+
82
+ Args:
83
+ query: Search query (full-text syntax supported by `search_notes`)
84
+ context: Optional FastMCP context passed through for auth/session data
85
+
86
+ Returns:
87
+ List with one dict: `{ "type": "text", "text": "{...JSON...}" }`
88
+ where the JSON body contains `results`, `total_count`, and echo of `query`.
89
+ """
90
+ logger.info(f"ChatGPT search request: query='{query}'")
91
+
92
+ try:
93
+ # Call underlying search_notes with sensible defaults for ChatGPT
94
+ results = await search_notes.fn(
95
+ query=query,
96
+ project=None, # Let project resolution happen automatically
97
+ page=1,
98
+ page_size=10, # Reasonable default for ChatGPT consumption
99
+ search_type="text", # Default to full-text search
100
+ context=context,
101
+ )
102
+
103
+ # Handle string error responses from search_notes
104
+ if isinstance(results, str):
105
+ logger.warning(f"Search failed with error: {results[:100]}...")
106
+ search_results = {
107
+ "results": [],
108
+ "error": "Search failed",
109
+ "error_details": results[:500], # Truncate long error messages
110
+ }
111
+ else:
112
+ # Format successful results for ChatGPT
113
+ formatted_results = _format_search_results_for_chatgpt(results)
114
+ search_results = {
115
+ "results": formatted_results,
116
+ "total_count": len(results.results), # Use actual count from results
117
+ "query": query,
118
+ }
119
+ logger.info(f"Search completed: {len(formatted_results)} results returned")
120
+
121
+ # Return in MCP content array format as required by OpenAI
122
+ return [{"type": "text", "text": json.dumps(search_results, ensure_ascii=False)}]
123
+
124
+ except Exception as e:
125
+ logger.error(f"ChatGPT search failed for query '{query}': {e}")
126
+ error_results = {
127
+ "results": [],
128
+ "error": "Internal search error",
129
+ "error_message": str(e)[:200],
130
+ }
131
+ return [{"type": "text", "text": json.dumps(error_results, ensure_ascii=False)}]
132
+
133
+
134
+ @mcp.tool(description="Fetch the full contents of a search result document")
135
+ async def fetch(
136
+ id: str,
137
+ context: Context | None = None,
138
+ ) -> List[Dict[str, Any]]:
139
+ """ChatGPT/OpenAI MCP fetch adapter returning a single text content item.
140
+
141
+ Args:
142
+ id: Document identifier (permalink, title, or memory URL)
143
+ context: Optional FastMCP context passed through for auth/session data
144
+
145
+ Returns:
146
+ List with one dict: `{ "type": "text", "text": "{...JSON...}" }`
147
+ where the JSON body includes `id`, `title`, `text`, `url`, and metadata.
148
+ """
149
+ logger.info(f"ChatGPT fetch request: id='{id}'")
150
+
151
+ try:
152
+ # Call underlying read_note function
153
+ content = await read_note.fn(
154
+ identifier=id,
155
+ project=None, # Let project resolution happen automatically
156
+ page=1,
157
+ page_size=10, # Default pagination
158
+ context=context,
159
+ )
160
+
161
+ # Format the document for ChatGPT
162
+ document = _format_document_for_chatgpt(content, id)
163
+
164
+ logger.info(f"Fetch completed: id='{id}', content_length={len(document.get('text', ''))}")
165
+
166
+ # Return in MCP content array format as required by OpenAI
167
+ return [{"type": "text", "text": json.dumps(document, ensure_ascii=False)}]
168
+
169
+ except Exception as e:
170
+ logger.error(f"ChatGPT fetch failed for id '{id}': {e}")
171
+ error_document = {
172
+ "id": id,
173
+ "title": "Fetch Error",
174
+ "text": f"Failed to fetch document: {str(e)[:200]}",
175
+ "url": id,
176
+ "metadata": {"error": "Fetch failed"},
177
+ }
178
+ return [{"type": "text", "text": json.dumps(error_document, ensure_ascii=False)}]
@@ -2,15 +2,16 @@ from textwrap import dedent
2
2
  from typing import Optional
3
3
 
4
4
  from loguru import logger
5
+ from fastmcp import Context
5
6
 
7
+ from basic_memory.mcp.project_context import get_active_project
6
8
  from basic_memory.mcp.tools.utils import call_delete
7
9
  from basic_memory.mcp.server import mcp
8
10
  from basic_memory.mcp.async_client import client
9
- from basic_memory.mcp.project_session import get_active_project
10
11
  from basic_memory.schemas import DeleteEntitiesResponse
11
12
 
12
13
 
13
- def _format_delete_error_response(error_message: str, identifier: str) -> str:
14
+ def _format_delete_error_response(project: str, error_message: str, identifier: str) -> str:
14
15
  """Format helpful error responses for delete failures that guide users to successful deletions."""
15
16
 
16
17
  # Note not found errors
@@ -24,7 +25,7 @@ def _format_delete_error_response(error_message: str, identifier: str) -> str:
24
25
  return dedent(f"""
25
26
  # Delete Failed - Note Not Found
26
27
 
27
- The note '{identifier}' could not be found for deletion.
28
+ The note '{identifier}' could not be found for deletion in {project}.
28
29
 
29
30
  ## This might mean:
30
31
  1. **Already deleted**: The note may have been deleted previously
@@ -32,21 +33,21 @@ def _format_delete_error_response(error_message: str, identifier: str) -> str:
32
33
  3. **Different project**: The note might be in a different project
33
34
 
34
35
  ## How to verify:
35
- 1. **Search for the note**: Use `search_notes("{search_term}")` to find it
36
+ 1. **Search for the note**: Use `search_notes("{project}", "{search_term}")` to find it
36
37
  2. **Try different formats**:
37
38
  - If you used a permalink like "folder/note-title", try just the title: "{title_format}"
38
39
  - If you used a title, try the permalink format: "{permalink_format}"
39
40
 
40
41
  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
+ 4. **List notes in project**: Use `list_directory("/")` to see what notes exist in the current project
42
43
 
43
44
  ## If the note actually exists:
44
45
  ```
45
46
  # First, find the correct identifier:
46
- search_notes("{identifier}")
47
+ search_notes("{project}", "{identifier}")
47
48
 
48
49
  # Then delete using the correct identifier:
49
- delete_note("correct-identifier-from-search")
50
+ delete_note("{project}", "correct-identifier-from-search")
50
51
  ```
51
52
 
52
53
  ## If you want to delete multiple similar notes:
@@ -69,12 +70,12 @@ You don't have permission to delete '{identifier}': {error_message}
69
70
  3. **Project access**: Ensure you're in the correct project with proper permissions
70
71
 
71
72
  ## 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}")`
73
+ - List available projects: `list_memory_projects()`
74
+ - Specify the correct project: `delete_note("{identifier}", project="project-name")`
75
+ - Verify note exists first: `read_note("{identifier}", project="project-name")`
75
76
 
76
77
  ## 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
+ Ask someone with write access to delete the note."""
78
79
 
79
80
  # Server/filesystem errors
80
81
  if (
@@ -92,8 +93,7 @@ A system error occurred while deleting '{identifier}': {error_message}
92
93
  3. **Check disk space**: Ensure the system has adequate storage
93
94
 
94
95
  ## Troubleshooting:
95
- - Verify note exists: `read_note("{identifier}")`
96
- - Check project status: `get_current_project()`
96
+ - Verify note exists: `read_note("{project}","{identifier}")`
97
97
  - Try again in a few moments
98
98
 
99
99
  ## If problem persists:
@@ -112,7 +112,7 @@ A database error occurred while deleting '{identifier}': {error_message}
112
112
 
113
113
  ## Steps to resolve:
114
114
  1. **Try again**: Wait a moment and retry the deletion
115
- 2. **Check note status**: `read_note("{identifier}")` to see current state
115
+ 2. **Check note status**: `read_note("{project}","{identifier}")` to see current state
116
116
  3. **Manual verification**: Use `list_directory()` to see if file still exists
117
117
 
118
118
  ## If the note appears gone but database shows it exists:
@@ -124,7 +124,7 @@ Send a message to support@basicmachines.co - a manual database cleanup may be ne
124
124
  Error deleting note '{identifier}': {error_message}
125
125
 
126
126
  ## General troubleshooting:
127
- 1. **Verify the note exists**: `read_note("{identifier}")` or `search_notes("{identifier}")`
127
+ 1. **Verify the note exists**: `read_note("{project}", "{identifier}")` or `search_notes("{project}", "{identifier}")`
128
128
  2. **Check permissions**: Ensure you can edit/delete files in this project
129
129
  3. **Try again**: The error might be temporary
130
130
  4. **Check project**: Make sure you're in the correct project
@@ -132,46 +132,77 @@ Error deleting note '{identifier}': {error_message}
132
132
  ## Step-by-step approach:
133
133
  ```
134
134
  # 1. Confirm note exists and get correct identifier
135
- search_notes("{identifier}")
135
+ search_notes("{project}", "{identifier}")
136
136
 
137
137
  # 2. Read the note to verify access
138
- read_note("correct-identifier-from-search")
138
+ read_note("{project}", "correct-identifier-from-search")
139
139
 
140
140
  # 3. Try deletion with correct identifier
141
- delete_note("correct-identifier-from-search")
141
+ delete_note("{project}", "correct-identifier-from-search")
142
142
  ```
143
143
 
144
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")`
145
+ - Check what notes exist: `list_directory("{project}", "/")`
148
146
 
149
147
  ## Need help?
150
- If the note should be deleted but the operation keeps failing, send a message to support@basicmachines.co."""
148
+ If the note should be deleted but the operation keeps failing, send a message to support@basicmemory.com."""
151
149
 
152
150
 
153
151
  @mcp.tool(description="Delete a note by title or permalink")
154
- async def delete_note(identifier: str, project: Optional[str] = None) -> bool | str:
152
+ async def delete_note(
153
+ identifier: str, project: Optional[str] = None, context: Context | None = None
154
+ ) -> bool | str:
155
155
  """Delete a note from the knowledge base.
156
156
 
157
+ Permanently removes a note from the specified project. The note is identified
158
+ by title or permalink. If the note doesn't exist, the operation returns False
159
+ without error. If deletion fails due to other issues, helpful error messages are provided.
160
+
161
+ Project Resolution:
162
+ Server resolves projects in this order: Single Project Mode → project parameter → default project.
163
+ If project unknown, use list_memory_projects() or recent_activity() first.
164
+
157
165
  Args:
158
- identifier: Note title or permalink
159
- project: Optional project name to delete from. If not provided, uses current active project.
166
+ project: Project name to delete from. Optional - server will resolve using hierarchy.
167
+ If unknown, use list_memory_projects() to discover available projects.
168
+ identifier: Note title or permalink to delete
169
+ Can be a title like "Meeting Notes" or permalink like "notes/meeting-notes"
170
+ context: Optional FastMCP context for performance caching.
160
171
 
161
172
  Returns:
162
- True if note was deleted, False otherwise
173
+ True if note was successfully deleted, False if note was not found.
174
+ On errors, returns a formatted string with helpful troubleshooting guidance.
163
175
 
164
176
  Examples:
165
177
  # Delete by title
166
- delete_note("Meeting Notes: Project Planning")
178
+ delete_note("my-project", "Meeting Notes: Project Planning")
167
179
 
168
180
  # Delete by permalink
169
- delete_note("notes/project-planning")
181
+ delete_note("work-docs", "notes/project-planning")
182
+
183
+ # Delete with exact path
184
+ delete_note("research", "experiments/ml-model-results")
185
+
186
+ # Common usage pattern
187
+ if delete_note("my-project", "old-draft"):
188
+ print("Note deleted successfully")
189
+ else:
190
+ print("Note not found or already deleted")
191
+
192
+ Raises:
193
+ HTTPError: If project doesn't exist or is inaccessible
194
+ SecurityError: If identifier attempts path traversal
195
+
196
+ Warning:
197
+ This operation is permanent and cannot be undone. The note file
198
+ will be removed from the filesystem and all references will be lost.
170
199
 
171
- # Delete from specific project
172
- delete_note("notes/project-planning", project="work-project")
200
+ Note:
201
+ If the note is not found, this function provides helpful error messages
202
+ with suggestions for finding the correct identifier, including search
203
+ commands and alternative formats to try.
173
204
  """
174
- active_project = get_active_project(project)
205
+ active_project = await get_active_project(client, project, context)
175
206
  project_url = active_project.project_url
176
207
 
177
208
  try:
@@ -179,13 +210,15 @@ async def delete_note(identifier: str, project: Optional[str] = None) -> bool |
179
210
  result = DeleteEntitiesResponse.model_validate(response.json())
180
211
 
181
212
  if result.deleted:
182
- logger.info(f"Successfully deleted note: {identifier}")
213
+ logger.info(
214
+ f"Successfully deleted note: {identifier} in project: {active_project.name}"
215
+ )
183
216
  return True
184
217
  else:
185
218
  logger.warning(f"Delete operation completed but note was not deleted: {identifier}")
186
219
  return False
187
220
 
188
221
  except Exception as e: # pragma: no cover
189
- logger.error(f"Delete failed for '{identifier}': {e}")
222
+ logger.error(f"Delete failed for '{identifier}': {e}, project: {active_project.name}")
190
223
  # Return formatted error message for better user experience
191
- return _format_delete_error_response(str(e), identifier)
224
+ return _format_delete_error_response(active_project.name, str(e), identifier)