basic-memory 0.7.0__py3-none-any.whl → 0.16.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.

Potentially problematic release.


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

Files changed (150) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +27 -3
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  6. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  7. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -0
  8. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  9. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  10. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  11. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +100 -0
  12. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  13. basic_memory/api/app.py +64 -18
  14. basic_memory/api/routers/__init__.py +4 -1
  15. basic_memory/api/routers/directory_router.py +84 -0
  16. basic_memory/api/routers/importer_router.py +152 -0
  17. basic_memory/api/routers/knowledge_router.py +166 -21
  18. basic_memory/api/routers/management_router.py +80 -0
  19. basic_memory/api/routers/memory_router.py +9 -64
  20. basic_memory/api/routers/project_router.py +406 -0
  21. basic_memory/api/routers/prompt_router.py +260 -0
  22. basic_memory/api/routers/resource_router.py +119 -4
  23. basic_memory/api/routers/search_router.py +5 -5
  24. basic_memory/api/routers/utils.py +130 -0
  25. basic_memory/api/template_loader.py +292 -0
  26. basic_memory/cli/app.py +43 -9
  27. basic_memory/cli/auth.py +277 -0
  28. basic_memory/cli/commands/__init__.py +13 -2
  29. basic_memory/cli/commands/cloud/__init__.py +6 -0
  30. basic_memory/cli/commands/cloud/api_client.py +112 -0
  31. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  32. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  33. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  34. basic_memory/cli/commands/cloud/rclone_commands.py +301 -0
  35. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  36. basic_memory/cli/commands/cloud/rclone_installer.py +249 -0
  37. basic_memory/cli/commands/cloud/upload.py +233 -0
  38. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  39. basic_memory/cli/commands/command_utils.py +51 -0
  40. basic_memory/cli/commands/db.py +28 -12
  41. basic_memory/cli/commands/import_chatgpt.py +40 -220
  42. basic_memory/cli/commands/import_claude_conversations.py +41 -168
  43. basic_memory/cli/commands/import_claude_projects.py +46 -157
  44. basic_memory/cli/commands/import_memory_json.py +48 -108
  45. basic_memory/cli/commands/mcp.py +84 -10
  46. basic_memory/cli/commands/project.py +876 -0
  47. basic_memory/cli/commands/status.py +50 -33
  48. basic_memory/cli/commands/tool.py +341 -0
  49. basic_memory/cli/main.py +8 -7
  50. basic_memory/config.py +477 -23
  51. basic_memory/db.py +168 -17
  52. basic_memory/deps.py +251 -25
  53. basic_memory/file_utils.py +113 -58
  54. basic_memory/ignore_utils.py +297 -0
  55. basic_memory/importers/__init__.py +27 -0
  56. basic_memory/importers/base.py +79 -0
  57. basic_memory/importers/chatgpt_importer.py +232 -0
  58. basic_memory/importers/claude_conversations_importer.py +177 -0
  59. basic_memory/importers/claude_projects_importer.py +148 -0
  60. basic_memory/importers/memory_json_importer.py +108 -0
  61. basic_memory/importers/utils.py +58 -0
  62. basic_memory/markdown/entity_parser.py +143 -23
  63. basic_memory/markdown/markdown_processor.py +3 -3
  64. basic_memory/markdown/plugins.py +39 -21
  65. basic_memory/markdown/schemas.py +1 -1
  66. basic_memory/markdown/utils.py +28 -13
  67. basic_memory/mcp/async_client.py +134 -4
  68. basic_memory/mcp/project_context.py +141 -0
  69. basic_memory/mcp/prompts/__init__.py +19 -0
  70. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  71. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  72. basic_memory/mcp/prompts/recent_activity.py +188 -0
  73. basic_memory/mcp/prompts/search.py +57 -0
  74. basic_memory/mcp/prompts/utils.py +162 -0
  75. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  76. basic_memory/mcp/resources/project_info.py +71 -0
  77. basic_memory/mcp/server.py +7 -13
  78. basic_memory/mcp/tools/__init__.py +33 -21
  79. basic_memory/mcp/tools/build_context.py +120 -0
  80. basic_memory/mcp/tools/canvas.py +130 -0
  81. basic_memory/mcp/tools/chatgpt_tools.py +187 -0
  82. basic_memory/mcp/tools/delete_note.py +225 -0
  83. basic_memory/mcp/tools/edit_note.py +320 -0
  84. basic_memory/mcp/tools/list_directory.py +167 -0
  85. basic_memory/mcp/tools/move_note.py +545 -0
  86. basic_memory/mcp/tools/project_management.py +200 -0
  87. basic_memory/mcp/tools/read_content.py +271 -0
  88. basic_memory/mcp/tools/read_note.py +255 -0
  89. basic_memory/mcp/tools/recent_activity.py +534 -0
  90. basic_memory/mcp/tools/search.py +369 -23
  91. basic_memory/mcp/tools/utils.py +374 -16
  92. basic_memory/mcp/tools/view_note.py +77 -0
  93. basic_memory/mcp/tools/write_note.py +207 -0
  94. basic_memory/models/__init__.py +3 -2
  95. basic_memory/models/knowledge.py +67 -15
  96. basic_memory/models/project.py +87 -0
  97. basic_memory/models/search.py +10 -6
  98. basic_memory/repository/__init__.py +2 -0
  99. basic_memory/repository/entity_repository.py +229 -7
  100. basic_memory/repository/observation_repository.py +35 -3
  101. basic_memory/repository/project_info_repository.py +10 -0
  102. basic_memory/repository/project_repository.py +103 -0
  103. basic_memory/repository/relation_repository.py +21 -2
  104. basic_memory/repository/repository.py +147 -29
  105. basic_memory/repository/search_repository.py +411 -62
  106. basic_memory/schemas/__init__.py +22 -9
  107. basic_memory/schemas/base.py +97 -8
  108. basic_memory/schemas/cloud.py +50 -0
  109. basic_memory/schemas/directory.py +30 -0
  110. basic_memory/schemas/importer.py +35 -0
  111. basic_memory/schemas/memory.py +187 -25
  112. basic_memory/schemas/project_info.py +211 -0
  113. basic_memory/schemas/prompt.py +90 -0
  114. basic_memory/schemas/request.py +56 -2
  115. basic_memory/schemas/response.py +1 -1
  116. basic_memory/schemas/search.py +31 -35
  117. basic_memory/schemas/sync_report.py +72 -0
  118. basic_memory/services/__init__.py +2 -1
  119. basic_memory/services/context_service.py +241 -104
  120. basic_memory/services/directory_service.py +295 -0
  121. basic_memory/services/entity_service.py +590 -60
  122. basic_memory/services/exceptions.py +21 -0
  123. basic_memory/services/file_service.py +284 -30
  124. basic_memory/services/initialization.py +191 -0
  125. basic_memory/services/link_resolver.py +49 -56
  126. basic_memory/services/project_service.py +863 -0
  127. basic_memory/services/search_service.py +168 -32
  128. basic_memory/sync/__init__.py +3 -2
  129. basic_memory/sync/background_sync.py +26 -0
  130. basic_memory/sync/sync_service.py +1180 -109
  131. basic_memory/sync/watch_service.py +412 -135
  132. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  133. basic_memory/templates/prompts/search.hbs +101 -0
  134. basic_memory/utils.py +383 -51
  135. basic_memory-0.16.1.dist-info/METADATA +493 -0
  136. basic_memory-0.16.1.dist-info/RECORD +148 -0
  137. {basic_memory-0.7.0.dist-info → basic_memory-0.16.1.dist-info}/entry_points.txt +1 -0
  138. basic_memory/alembic/README +0 -1
  139. basic_memory/cli/commands/sync.py +0 -206
  140. basic_memory/cli/commands/tools.py +0 -157
  141. basic_memory/mcp/tools/knowledge.py +0 -68
  142. basic_memory/mcp/tools/memory.py +0 -170
  143. basic_memory/mcp/tools/notes.py +0 -202
  144. basic_memory/schemas/discovery.py +0 -28
  145. basic_memory/sync/file_change_scanner.py +0 -158
  146. basic_memory/sync/utils.py +0 -31
  147. basic_memory-0.7.0.dist-info/METADATA +0 -378
  148. basic_memory-0.7.0.dist-info/RECORD +0 -82
  149. {basic_memory-0.7.0.dist-info → basic_memory-0.16.1.dist-info}/WHEEL +0 -0
  150. {basic_memory-0.7.0.dist-info → basic_memory-0.16.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,120 @@
1
+ """Build context tool for Basic Memory MCP server."""
2
+
3
+ from typing import Optional
4
+
5
+ from loguru import logger
6
+ from fastmcp import Context
7
+
8
+ from basic_memory.mcp.async_client import get_client
9
+ from basic_memory.mcp.project_context import get_active_project
10
+ from basic_memory.mcp.server import mcp
11
+ from basic_memory.mcp.tools.utils import call_get
12
+ from basic_memory.schemas.base import TimeFrame
13
+ from basic_memory.schemas.memory import (
14
+ GraphContext,
15
+ MemoryUrl,
16
+ memory_url_path,
17
+ )
18
+
19
+
20
+ @mcp.tool(
21
+ description="""Build context from a memory:// URI to continue conversations naturally.
22
+
23
+ Use this to follow up on previous discussions or explore related topics.
24
+
25
+ Memory URL Format:
26
+ - Use paths like "folder/note" or "memory://folder/note"
27
+ - Pattern matching: "folder/*" matches all notes in folder
28
+ - Valid characters: letters, numbers, hyphens, underscores, forward slashes
29
+ - Avoid: double slashes (//), angle brackets (<>), quotes, pipes (|)
30
+ - Examples: "specs/search", "projects/basic-memory", "notes/*"
31
+
32
+ Timeframes support natural language like:
33
+ - "2 days ago", "last week", "today", "3 months ago"
34
+ - Or standard formats like "7d", "24h"
35
+ """,
36
+ )
37
+ async def build_context(
38
+ url: MemoryUrl,
39
+ project: Optional[str] = None,
40
+ depth: str | int | None = 1,
41
+ timeframe: Optional[TimeFrame] = "7d",
42
+ page: int = 1,
43
+ page_size: int = 10,
44
+ max_related: int = 10,
45
+ context: Context | None = None,
46
+ ) -> GraphContext:
47
+ """Get context needed to continue a discussion within a specific project.
48
+
49
+ This tool enables natural continuation of discussions by loading relevant context
50
+ from memory:// URIs. It uses pattern matching to find relevant content and builds
51
+ a rich context graph of related information.
52
+
53
+ Project Resolution:
54
+ Server resolves projects in this order: Single Project Mode → project parameter → default project.
55
+ If project unknown, use list_memory_projects() or recent_activity() first.
56
+
57
+ Args:
58
+ project: Project name to build context from. Optional - server will resolve using hierarchy.
59
+ If unknown, use list_memory_projects() to discover available projects.
60
+ url: memory:// URI pointing to discussion content (e.g. memory://specs/search)
61
+ depth: How many relation hops to traverse (1-3 recommended for performance)
62
+ timeframe: How far back to look. Supports natural language like "2 days ago", "last week"
63
+ page: Page number of results to return (default: 1)
64
+ page_size: Number of results to return per page (default: 10)
65
+ max_related: Maximum number of related results to return (default: 10)
66
+ context: Optional FastMCP context for performance caching.
67
+
68
+ Returns:
69
+ GraphContext containing:
70
+ - primary_results: Content matching the memory:// URI
71
+ - related_results: Connected content via relations
72
+ - metadata: Context building details
73
+
74
+ Examples:
75
+ # Continue a specific discussion
76
+ build_context("my-project", "memory://specs/search")
77
+
78
+ # Get deeper context about a component
79
+ build_context("work-docs", "memory://components/memory-service", depth=2)
80
+
81
+ # Look at recent changes to a specification
82
+ build_context("research", "memory://specs/document-format", timeframe="today")
83
+
84
+ # Research the history of a feature
85
+ build_context("dev-notes", "memory://features/knowledge-graph", timeframe="3 months ago")
86
+
87
+ Raises:
88
+ ToolError: If project doesn't exist or depth parameter is invalid
89
+ """
90
+ logger.info(f"Building context from {url} in project {project}")
91
+
92
+ # Convert string depth to integer if needed
93
+ if isinstance(depth, str):
94
+ try:
95
+ depth = int(depth)
96
+ except ValueError:
97
+ from mcp.server.fastmcp.exceptions import ToolError
98
+
99
+ raise ToolError(f"Invalid depth parameter: '{depth}' is not a valid integer")
100
+
101
+ # URL is already validated and normalized by MemoryUrl type annotation
102
+
103
+ async with get_client() as client:
104
+ # Get the active project using the new stateless approach
105
+ active_project = await get_active_project(client, project, context)
106
+
107
+ project_url = active_project.project_url
108
+
109
+ response = await call_get(
110
+ client,
111
+ f"{project_url}/memory/{memory_url_path(url)}",
112
+ params={
113
+ "depth": depth,
114
+ "timeframe": timeframe,
115
+ "page": page,
116
+ "page_size": page_size,
117
+ "max_related": max_related,
118
+ },
119
+ )
120
+ return GraphContext.model_validate(response.json())
@@ -0,0 +1,130 @@
1
+ """Canvas creation tool for Basic Memory MCP server.
2
+
3
+ This tool creates Obsidian canvas files (.canvas) using the JSON Canvas 1.0 spec.
4
+ """
5
+
6
+ import json
7
+ from typing import Dict, List, Any, Optional
8
+
9
+ from loguru import logger
10
+ from fastmcp import Context
11
+
12
+ from basic_memory.mcp.async_client import get_client
13
+ from basic_memory.mcp.project_context import get_active_project
14
+ from basic_memory.mcp.server import mcp
15
+ from basic_memory.mcp.tools.utils import call_put
16
+
17
+
18
+ @mcp.tool(
19
+ description="Create an Obsidian canvas file to visualize concepts and connections.",
20
+ )
21
+ async def canvas(
22
+ nodes: List[Dict[str, Any]],
23
+ edges: List[Dict[str, Any]],
24
+ title: str,
25
+ folder: str,
26
+ project: Optional[str] = None,
27
+ context: Context | None = None,
28
+ ) -> str:
29
+ """Create an Obsidian canvas file with the provided nodes and edges.
30
+
31
+ This tool creates a .canvas file compatible with Obsidian's Canvas feature,
32
+ allowing visualization of relationships between concepts or documents.
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
+
38
+ For the full JSON Canvas 1.0 specification, see the 'spec://canvas' resource.
39
+
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.
43
+ nodes: List of node objects following JSON Canvas 1.0 spec
44
+ edges: List of edge objects following JSON Canvas 1.0 spec
45
+ title: The title of the canvas (will be saved as title.canvas)
46
+ folder: Folder path relative to project root where the canvas should be saved.
47
+ Use forward slashes (/) as separators. Examples: "diagrams", "projects/2025", "visual/maps"
48
+ context: Optional FastMCP context for performance caching.
49
+
50
+ Returns:
51
+ A summary of the created canvas file
52
+
53
+ Important Notes:
54
+ - When referencing files, use the exact file path as shown in Obsidian
55
+ Example: "folder/Document Name.md" (not permalink format)
56
+ - For file nodes, the "file" attribute must reference an existing file
57
+ - Nodes require id, type, x, y, width, height properties
58
+ - Edges require id, fromNode, toNode properties
59
+ - Position nodes in a logical layout (x,y coordinates in pixels)
60
+ - Use color attributes ("1"-"6" or hex) for visual organization
61
+
62
+ Basic Structure:
63
+ ```json
64
+ {
65
+ "nodes": [
66
+ {
67
+ "id": "node1",
68
+ "type": "file", // Options: "file", "text", "link", "group"
69
+ "file": "folder/Document.md",
70
+ "x": 0,
71
+ "y": 0,
72
+ "width": 400,
73
+ "height": 300
74
+ }
75
+ ],
76
+ "edges": [
77
+ {
78
+ "id": "edge1",
79
+ "fromNode": "node1",
80
+ "toNode": "node2",
81
+ "label": "connects to"
82
+ }
83
+ ]
84
+ }
85
+ ```
86
+
87
+ Examples:
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")
93
+
94
+ Raises:
95
+ ToolError: If project doesn't exist or folder path is invalid
96
+ """
97
+ async with get_client() as client:
98
+ active_project = await get_active_project(client, project, context)
99
+ project_url = active_project.project_url
100
+
101
+ # Ensure path has .canvas extension
102
+ file_title = title if title.endswith(".canvas") else f"{title}.canvas"
103
+ file_path = f"{folder}/{file_title}"
104
+
105
+ # Create canvas data structure
106
+ canvas_data = {"nodes": nodes, "edges": edges}
107
+
108
+ # Convert to JSON
109
+ canvas_json = json.dumps(canvas_data, indent=2)
110
+
111
+ # Write the file using the resource API
112
+ logger.info(f"Creating canvas file: {file_path} in project {project}")
113
+ # Send canvas_json as content string, not as json parameter
114
+ # The resource endpoint expects Body() string content, not JSON-encoded data
115
+ response = await call_put(
116
+ client,
117
+ f"{project_url}/resource/{file_path}",
118
+ content=canvas_json,
119
+ headers={"Content-Type": "text/plain"},
120
+ )
121
+
122
+ # Parse response
123
+ result = response.json()
124
+ logger.debug(result)
125
+
126
+ # Build summary
127
+ action = "Created" if response.status_code == 201 else "Updated"
128
+ summary = [f"# {action}: {file_path}", "\nThe canvas is ready to open in Obsidian."]
129
+
130
+ return "\n".join(summary)
@@ -0,0 +1,187 @@
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
+ from basic_memory.config import ConfigManager
18
+
19
+
20
+ def _format_search_results_for_chatgpt(results: SearchResponse) -> List[Dict[str, Any]]:
21
+ """Format search results according to ChatGPT's expected schema.
22
+
23
+ Returns a list of result objects with id, title, and url fields.
24
+ """
25
+ formatted_results = []
26
+
27
+ for result in results.results:
28
+ formatted_result = {
29
+ "id": result.permalink or f"doc-{len(formatted_results)}",
30
+ "title": result.title if result.title and result.title.strip() else "Untitled",
31
+ "url": result.permalink or "",
32
+ }
33
+ formatted_results.append(formatted_result)
34
+
35
+ return formatted_results
36
+
37
+
38
+ def _format_document_for_chatgpt(
39
+ content: str, identifier: str, title: Optional[str] = None
40
+ ) -> Dict[str, Any]:
41
+ """Format document content according to ChatGPT's expected schema.
42
+
43
+ Returns a document object with id, title, text, url, and metadata fields.
44
+ """
45
+ # Extract title from markdown content if not provided
46
+ if not title and isinstance(content, str):
47
+ lines = content.split("\n")
48
+ if lines and lines[0].startswith("# "):
49
+ title = lines[0][2:].strip()
50
+ else:
51
+ title = identifier.split("/")[-1].replace("-", " ").title()
52
+
53
+ # Ensure title is never None
54
+ if not title:
55
+ title = "Untitled Document"
56
+
57
+ # Handle error cases
58
+ if isinstance(content, str) and content.startswith("# Note Not Found"):
59
+ return {
60
+ "id": identifier,
61
+ "title": title or "Document Not Found",
62
+ "text": content,
63
+ "url": identifier,
64
+ "metadata": {"error": "Document not found"},
65
+ }
66
+
67
+ return {
68
+ "id": identifier,
69
+ "title": title or "Untitled Document",
70
+ "text": content,
71
+ "url": identifier,
72
+ "metadata": {"format": "markdown"},
73
+ }
74
+
75
+
76
+ @mcp.tool(description="Search for content across the knowledge base")
77
+ async def search(
78
+ query: str,
79
+ context: Context | None = None,
80
+ ) -> List[Dict[str, Any]]:
81
+ """ChatGPT/OpenAI MCP search adapter returning a single text content item.
82
+
83
+ Args:
84
+ query: Search query (full-text syntax supported by `search_notes`)
85
+ context: Optional FastMCP context passed through for auth/session data
86
+
87
+ Returns:
88
+ List with one dict: `{ "type": "text", "text": "{...JSON...}" }`
89
+ where the JSON body contains `results`, `total_count`, and echo of `query`.
90
+ """
91
+ logger.info(f"ChatGPT search request: query='{query}'")
92
+
93
+ try:
94
+ # ChatGPT tools don't expose project parameter, so use default project
95
+ config = ConfigManager().config
96
+ default_project = config.default_project
97
+
98
+ # Call underlying search_notes with sensible defaults for ChatGPT
99
+ results = await search_notes.fn(
100
+ query=query,
101
+ project=default_project, # Use default project for ChatGPT
102
+ page=1,
103
+ page_size=10, # Reasonable default for ChatGPT consumption
104
+ search_type="text", # Default to full-text search
105
+ context=context,
106
+ )
107
+
108
+ # Handle string error responses from search_notes
109
+ if isinstance(results, str):
110
+ logger.warning(f"Search failed with error: {results[:100]}...")
111
+ search_results = {
112
+ "results": [],
113
+ "error": "Search failed",
114
+ "error_details": results[:500], # Truncate long error messages
115
+ }
116
+ else:
117
+ # Format successful results for ChatGPT
118
+ formatted_results = _format_search_results_for_chatgpt(results)
119
+ search_results = {
120
+ "results": formatted_results,
121
+ "total_count": len(results.results), # Use actual count from results
122
+ "query": query,
123
+ }
124
+ logger.info(f"Search completed: {len(formatted_results)} results returned")
125
+
126
+ # Return in MCP content array format as required by OpenAI
127
+ return [{"type": "text", "text": json.dumps(search_results, ensure_ascii=False)}]
128
+
129
+ except Exception as e:
130
+ logger.error(f"ChatGPT search failed for query '{query}': {e}")
131
+ error_results = {
132
+ "results": [],
133
+ "error": "Internal search error",
134
+ "error_message": str(e)[:200],
135
+ }
136
+ return [{"type": "text", "text": json.dumps(error_results, ensure_ascii=False)}]
137
+
138
+
139
+ @mcp.tool(description="Fetch the full contents of a search result document")
140
+ async def fetch(
141
+ id: str,
142
+ context: Context | None = None,
143
+ ) -> List[Dict[str, Any]]:
144
+ """ChatGPT/OpenAI MCP fetch adapter returning a single text content item.
145
+
146
+ Args:
147
+ id: Document identifier (permalink, title, or memory URL)
148
+ context: Optional FastMCP context passed through for auth/session data
149
+
150
+ Returns:
151
+ List with one dict: `{ "type": "text", "text": "{...JSON...}" }`
152
+ where the JSON body includes `id`, `title`, `text`, `url`, and metadata.
153
+ """
154
+ logger.info(f"ChatGPT fetch request: id='{id}'")
155
+
156
+ try:
157
+ # ChatGPT tools don't expose project parameter, so use default project
158
+ config = ConfigManager().config
159
+ default_project = config.default_project
160
+
161
+ # Call underlying read_note function
162
+ content = await read_note.fn(
163
+ identifier=id,
164
+ project=default_project, # Use default project for ChatGPT
165
+ page=1,
166
+ page_size=10, # Default pagination
167
+ context=context,
168
+ )
169
+
170
+ # Format the document for ChatGPT
171
+ document = _format_document_for_chatgpt(content, id)
172
+
173
+ logger.info(f"Fetch completed: id='{id}', content_length={len(document.get('text', ''))}")
174
+
175
+ # Return in MCP content array format as required by OpenAI
176
+ return [{"type": "text", "text": json.dumps(document, ensure_ascii=False)}]
177
+
178
+ except Exception as e:
179
+ logger.error(f"ChatGPT fetch failed for id '{id}': {e}")
180
+ error_document = {
181
+ "id": id,
182
+ "title": "Fetch Error",
183
+ "text": f"Failed to fetch document: {str(e)[:200]}",
184
+ "url": id,
185
+ "metadata": {"error": "Fetch failed"},
186
+ }
187
+ return [{"type": "text", "text": json.dumps(error_document, ensure_ascii=False)}]
@@ -0,0 +1,225 @@
1
+ from textwrap import dedent
2
+ from typing import Optional
3
+
4
+ from loguru import logger
5
+ from fastmcp import Context
6
+
7
+ from basic_memory.mcp.project_context import get_active_project
8
+ from basic_memory.mcp.tools.utils import call_delete
9
+ from basic_memory.mcp.server import mcp
10
+ from basic_memory.mcp.async_client import get_client
11
+ from basic_memory.schemas import DeleteEntitiesResponse
12
+
13
+
14
+ def _format_delete_error_response(project: str, error_message: str, identifier: str) -> str:
15
+ """Format helpful error responses for delete failures that guide users to successful deletions."""
16
+
17
+ # Note not found errors
18
+ if "entity not found" in error_message.lower() or "not found" in error_message.lower():
19
+ search_term = identifier.split("/")[-1] if "/" in identifier else identifier
20
+ title_format = (
21
+ identifier.split("/")[-1].replace("-", " ").title() if "/" in identifier else identifier
22
+ )
23
+ permalink_format = identifier.lower().replace(" ", "-")
24
+
25
+ return dedent(f"""
26
+ # Delete Failed - Note Not Found
27
+
28
+ The note '{identifier}' could not be found for deletion in {project}.
29
+
30
+ ## This might mean:
31
+ 1. **Already deleted**: The note may have been deleted previously
32
+ 2. **Wrong identifier**: The identifier format might be incorrect
33
+ 3. **Different project**: The note might be in a different project
34
+
35
+ ## How to verify:
36
+ 1. **Search for the note**: Use `search_notes("{project}", "{search_term}")` to find it
37
+ 2. **Try different formats**:
38
+ - If you used a permalink like "folder/note-title", try just the title: "{title_format}"
39
+ - If you used a title, try the permalink format: "{permalink_format}"
40
+
41
+ 3. **Check if already deleted**: Use `list_directory("/")` to see what notes exist
42
+ 4. **List notes in project**: Use `list_directory("/")` to see what notes exist in the current project
43
+
44
+ ## If the note actually exists:
45
+ ```
46
+ # First, find the correct identifier:
47
+ search_notes("{project}", "{identifier}")
48
+
49
+ # Then delete using the correct identifier:
50
+ delete_note("{project}", "correct-identifier-from-search")
51
+ ```
52
+
53
+ ## If you want to delete multiple similar notes:
54
+ Use search to find all related notes and delete them one by one.
55
+ """).strip()
56
+
57
+ # Permission/access errors
58
+ if (
59
+ "permission" in error_message.lower()
60
+ or "access" in error_message.lower()
61
+ or "forbidden" in error_message.lower()
62
+ ):
63
+ return f"""# Delete Failed - Permission Error
64
+
65
+ You don't have permission to delete '{identifier}': {error_message}
66
+
67
+ ## How to resolve:
68
+ 1. **Check permissions**: Verify you have delete/write access to this project
69
+ 2. **File locks**: The note might be open in another application
70
+ 3. **Project access**: Ensure you're in the correct project with proper permissions
71
+
72
+ ## Alternative actions:
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")`
76
+
77
+ ## If you have read-only access:
78
+ Ask someone with write access to delete the note."""
79
+
80
+ # Server/filesystem errors
81
+ if (
82
+ "server error" in error_message.lower()
83
+ or "filesystem" in error_message.lower()
84
+ or "disk" in error_message.lower()
85
+ ):
86
+ return f"""# Delete Failed - System Error
87
+
88
+ A system error occurred while deleting '{identifier}': {error_message}
89
+
90
+ ## Immediate steps:
91
+ 1. **Try again**: The error might be temporary
92
+ 2. **Check file status**: Verify the file isn't locked or in use
93
+ 3. **Check disk space**: Ensure the system has adequate storage
94
+
95
+ ## Troubleshooting:
96
+ - Verify note exists: `read_note("{project}","{identifier}")`
97
+ - Try again in a few moments
98
+
99
+ ## If problem persists:
100
+ Send a message to support@basicmachines.co - there may be a filesystem or database issue."""
101
+
102
+ # Database/sync errors
103
+ if "database" in error_message.lower() or "sync" in error_message.lower():
104
+ return f"""# Delete Failed - Database Error
105
+
106
+ A database error occurred while deleting '{identifier}': {error_message}
107
+
108
+ ## This usually means:
109
+ 1. **Sync conflict**: The file system and database are out of sync
110
+ 2. **Database lock**: Another operation is accessing the database
111
+ 3. **Corrupted entry**: The database entry might be corrupted
112
+
113
+ ## Steps to resolve:
114
+ 1. **Try again**: Wait a moment and retry the deletion
115
+ 2. **Check note status**: `read_note("{project}","{identifier}")` to see current state
116
+ 3. **Manual verification**: Use `list_directory()` to see if file still exists
117
+
118
+ ## If the note appears gone but database shows it exists:
119
+ Send a message to support@basicmachines.co - a manual database cleanup may be needed."""
120
+
121
+ # Generic fallback
122
+ return f"""# Delete Failed
123
+
124
+ Error deleting note '{identifier}': {error_message}
125
+
126
+ ## General troubleshooting:
127
+ 1. **Verify the note exists**: `read_note("{project}", "{identifier}")` or `search_notes("{project}", "{identifier}")`
128
+ 2. **Check permissions**: Ensure you can edit/delete files in this project
129
+ 3. **Try again**: The error might be temporary
130
+ 4. **Check project**: Make sure you're in the correct project
131
+
132
+ ## Step-by-step approach:
133
+ ```
134
+ # 1. Confirm note exists and get correct identifier
135
+ search_notes("{project}", "{identifier}")
136
+
137
+ # 2. Read the note to verify access
138
+ read_note("{project}", "correct-identifier-from-search")
139
+
140
+ # 3. Try deletion with correct identifier
141
+ delete_note("{project}", "correct-identifier-from-search")
142
+ ```
143
+
144
+ ## Alternative approaches:
145
+ - Check what notes exist: `list_directory("{project}", "/")`
146
+
147
+ ## Need help?
148
+ If the note should be deleted but the operation keeps failing, send a message to support@basicmemory.com."""
149
+
150
+
151
+ @mcp.tool(description="Delete a note by title or permalink")
152
+ async def delete_note(
153
+ identifier: str, project: Optional[str] = None, context: Context | None = None
154
+ ) -> bool | str:
155
+ """Delete a note from the knowledge base.
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
+
165
+ Args:
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.
171
+
172
+ Returns:
173
+ True if note was successfully deleted, False if note was not found.
174
+ On errors, returns a formatted string with helpful troubleshooting guidance.
175
+
176
+ Examples:
177
+ # Delete by title
178
+ delete_note("my-project", "Meeting Notes: Project Planning")
179
+
180
+ # Delete by permalink
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.
199
+
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.
204
+ """
205
+ async with get_client() as client:
206
+ active_project = await get_active_project(client, project, context)
207
+ project_url = active_project.project_url
208
+
209
+ try:
210
+ response = await call_delete(client, f"{project_url}/knowledge/entities/{identifier}")
211
+ result = DeleteEntitiesResponse.model_validate(response.json())
212
+
213
+ if result.deleted:
214
+ logger.info(
215
+ f"Successfully deleted note: {identifier} in project: {active_project.name}"
216
+ )
217
+ return True
218
+ else:
219
+ logger.warning(f"Delete operation completed but note was not deleted: {identifier}")
220
+ return False
221
+
222
+ except Exception as e: # pragma: no cover
223
+ logger.error(f"Delete failed for '{identifier}': {e}, project: {active_project.name}")
224
+ # Return formatted error message for better user experience
225
+ return _format_delete_error_response(active_project.name, str(e), identifier)