basic-memory 0.13.0b4__py3-none-any.whl → 0.13.0b5__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 (39) hide show
  1. basic_memory/__init__.py +1 -7
  2. basic_memory/api/routers/knowledge_router.py +13 -0
  3. basic_memory/api/routers/memory_router.py +3 -4
  4. basic_memory/api/routers/project_router.py +6 -5
  5. basic_memory/api/routers/prompt_router.py +2 -2
  6. basic_memory/cli/commands/project.py +2 -2
  7. basic_memory/cli/commands/status.py +1 -1
  8. basic_memory/cli/commands/sync.py +1 -1
  9. basic_memory/mcp/prompts/__init__.py +2 -0
  10. basic_memory/mcp/prompts/sync_status.py +116 -0
  11. basic_memory/mcp/server.py +6 -6
  12. basic_memory/mcp/tools/__init__.py +4 -0
  13. basic_memory/mcp/tools/build_context.py +32 -7
  14. basic_memory/mcp/tools/canvas.py +2 -1
  15. basic_memory/mcp/tools/delete_note.py +159 -4
  16. basic_memory/mcp/tools/edit_note.py +17 -11
  17. basic_memory/mcp/tools/move_note.py +252 -40
  18. basic_memory/mcp/tools/project_management.py +35 -3
  19. basic_memory/mcp/tools/read_note.py +9 -2
  20. basic_memory/mcp/tools/search.py +180 -8
  21. basic_memory/mcp/tools/sync_status.py +254 -0
  22. basic_memory/mcp/tools/utils.py +47 -0
  23. basic_memory/mcp/tools/view_note.py +66 -0
  24. basic_memory/mcp/tools/write_note.py +13 -2
  25. basic_memory/repository/search_repository.py +99 -26
  26. basic_memory/schemas/base.py +33 -5
  27. basic_memory/schemas/memory.py +58 -1
  28. basic_memory/services/entity_service.py +4 -4
  29. basic_memory/services/initialization.py +32 -5
  30. basic_memory/services/link_resolver.py +20 -5
  31. basic_memory/services/migration_service.py +168 -0
  32. basic_memory/services/project_service.py +97 -47
  33. basic_memory/services/sync_status_service.py +181 -0
  34. basic_memory/sync/sync_service.py +55 -2
  35. {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/METADATA +2 -2
  36. {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/RECORD +39 -34
  37. {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/WHEEL +0 -0
  38. {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/entry_points.txt +0 -0
  39. {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/licenses/LICENSE +0 -0
basic_memory/__init__.py CHANGED
@@ -1,9 +1,3 @@
1
1
  """basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
2
2
 
3
- try:
4
- from importlib.metadata import version
5
-
6
- __version__ = version("basic-memory")
7
- except Exception: # pragma: no cover
8
- # Fallback if package not installed (e.g., during development)
9
- __version__ = "0.0.0" # pragma: no cover
3
+ __version__ = "0.13.0b5"
@@ -14,6 +14,7 @@ from basic_memory.deps import (
14
14
  FileServiceDep,
15
15
  ProjectConfigDep,
16
16
  AppConfigDep,
17
+ SyncServiceDep,
17
18
  )
18
19
  from basic_memory.schemas import (
19
20
  EntityListResponse,
@@ -63,6 +64,7 @@ async def create_or_update_entity(
63
64
  entity_service: EntityServiceDep,
64
65
  search_service: SearchServiceDep,
65
66
  file_service: FileServiceDep,
67
+ sync_service: SyncServiceDep,
66
68
  ) -> EntityResponse:
67
69
  """Create or update an entity. If entity exists, it will be updated, otherwise created."""
68
70
  logger.info(
@@ -85,6 +87,17 @@ async def create_or_update_entity(
85
87
 
86
88
  # reindex
87
89
  await search_service.index_entity(entity, background_tasks=background_tasks)
90
+
91
+ # Attempt immediate relation resolution when creating new entities
92
+ # This helps resolve forward references when related entities are created in the same session
93
+ if created:
94
+ try:
95
+ await sync_service.resolve_relations()
96
+ logger.debug(f"Resolved relations after creating entity: {entity.permalink}")
97
+ except Exception as e: # pragma: no cover
98
+ # Don't fail the entire request if relation resolution fails
99
+ logger.warning(f"Failed to resolve relations after entity creation: {e}")
100
+
88
101
  result = EntityResponse.model_validate(entity)
89
102
 
90
103
  logger.info(
@@ -2,12 +2,11 @@
2
2
 
3
3
  from typing import Annotated, Optional
4
4
 
5
- from dateparser import parse
6
5
  from fastapi import APIRouter, Query
7
6
  from loguru import logger
8
7
 
9
8
  from basic_memory.deps import ContextServiceDep, EntityRepositoryDep
10
- from basic_memory.schemas.base import TimeFrame
9
+ from basic_memory.schemas.base import TimeFrame, parse_timeframe
11
10
  from basic_memory.schemas.memory import (
12
11
  GraphContext,
13
12
  normalize_memory_url,
@@ -40,7 +39,7 @@ async def recent(
40
39
  f"Getting recent context: `{types}` depth: `{depth}` timeframe: `{timeframe}` page: `{page}` page_size: `{page_size}` max_related: `{max_related}`"
41
40
  )
42
41
  # Parse timeframe
43
- since = parse(timeframe)
42
+ since = parse_timeframe(timeframe)
44
43
  limit = page_size
45
44
  offset = (page - 1) * page_size
46
45
 
@@ -78,7 +77,7 @@ async def get_memory_context(
78
77
  memory_url = normalize_memory_url(uri)
79
78
 
80
79
  # Parse timeframe
81
- since = parse(timeframe) if timeframe else None
80
+ since = parse_timeframe(timeframe) if timeframe else None
82
81
  limit = page_size
83
82
  offset = (page - 1) * page_size
84
83
 
@@ -3,7 +3,7 @@
3
3
  from fastapi import APIRouter, HTTPException, Path, Body
4
4
  from typing import Optional
5
5
 
6
- from basic_memory.deps import ProjectServiceDep
6
+ from basic_memory.deps import ProjectServiceDep, ProjectPathDep
7
7
  from basic_memory.schemas import ProjectInfoResponse
8
8
  from basic_memory.schemas.project_info import (
9
9
  ProjectList,
@@ -22,9 +22,10 @@ project_resource_router = APIRouter(prefix="/projects", tags=["project_managemen
22
22
  @project_router.get("/info", response_model=ProjectInfoResponse)
23
23
  async def get_project_info(
24
24
  project_service: ProjectServiceDep,
25
+ project: ProjectPathDep,
25
26
  ) -> ProjectInfoResponse:
26
- """Get comprehensive information about the current Basic Memory project."""
27
- return await project_service.get_project_info()
27
+ """Get comprehensive information about the specified Basic Memory project."""
28
+ return await project_service.get_project_info(project)
28
29
 
29
30
 
30
31
  # Update a project
@@ -47,7 +48,7 @@ async def update_project(
47
48
  """
48
49
  try: # pragma: no cover
49
50
  # Get original project info for the response
50
- old_project = ProjectItem(
51
+ old_project_info = ProjectItem(
51
52
  name=project_name,
52
53
  path=project_service.projects.get(project_name, ""),
53
54
  )
@@ -61,7 +62,7 @@ async def update_project(
61
62
  message=f"Project '{project_name}' updated successfully",
62
63
  status="success",
63
64
  default=(project_name == project_service.default_project),
64
- old_project=old_project,
65
+ old_project=old_project_info,
65
66
  new_project=ProjectItem(name=project_name, path=updated_path),
66
67
  )
67
68
  except ValueError as e: # pragma: no cover
@@ -5,12 +5,12 @@ It centralizes all prompt formatting logic that was previously in the MCP prompt
5
5
  """
6
6
 
7
7
  from datetime import datetime, timezone
8
- from dateparser import parse
9
8
  from fastapi import APIRouter, HTTPException, status
10
9
  from loguru import logger
11
10
 
12
11
  from basic_memory.api.routers.utils import to_graph_context, to_search_results
13
12
  from basic_memory.api.template_loader import template_loader
13
+ from basic_memory.schemas.base import parse_timeframe
14
14
  from basic_memory.deps import (
15
15
  ContextServiceDep,
16
16
  EntityRepositoryDep,
@@ -51,7 +51,7 @@ async def continue_conversation(
51
51
  f"Generating continue conversation prompt, topic: {request.topic}, timeframe: {request.timeframe}"
52
52
  )
53
53
 
54
- since = parse(request.timeframe) if request.timeframe else None
54
+ since = parse_timeframe(request.timeframe) if request.timeframe else None
55
55
 
56
56
  # Initialize search results
57
57
  search_results = []
@@ -221,7 +221,7 @@ def display_project_info(
221
221
  console.print(entity_types_table)
222
222
 
223
223
  # Most connected entities
224
- if info.statistics.most_connected_entities:
224
+ if info.statistics.most_connected_entities: # pragma: no cover
225
225
  connected_table = Table(title="🔗 Most Connected Entities")
226
226
  connected_table.add_column("Title", style="blue")
227
227
  connected_table.add_column("Permalink", style="cyan")
@@ -235,7 +235,7 @@ def display_project_info(
235
235
  console.print(connected_table)
236
236
 
237
237
  # Recent activity
238
- if info.activity.recently_updated:
238
+ if info.activity.recently_updated: # pragma: no cover
239
239
  recent_table = Table(title="🕒 Recent Activity")
240
240
  recent_table.add_column("Title", style="blue")
241
241
  recent_table.add_column("Type", style="cyan")
@@ -122,7 +122,7 @@ def display_changes(project_name: str, title: str, changes: SyncReport, verbose:
122
122
  console.print(Panel(tree, expand=False))
123
123
 
124
124
 
125
- async def run_status(verbose: bool = False):
125
+ async def run_status(verbose: bool = False): # pragma: no cover
126
126
  """Check sync status of files vs database."""
127
127
  # Check knowledge/ directory
128
128
 
@@ -180,7 +180,7 @@ async def run_sync(verbose: bool = False):
180
180
  sync_service = await get_sync_service(project)
181
181
 
182
182
  logger.info("Running one-time sync")
183
- knowledge_changes = await sync_service.sync(config.home)
183
+ knowledge_changes = await sync_service.sync(config.home, project_name=project.name)
184
184
 
185
185
  # Log results
186
186
  duration_ms = int((time.time() - start_time) * 1000)
@@ -10,10 +10,12 @@ from basic_memory.mcp.prompts import continue_conversation
10
10
  from basic_memory.mcp.prompts import recent_activity
11
11
  from basic_memory.mcp.prompts import search
12
12
  from basic_memory.mcp.prompts import ai_assistant_guide
13
+ from basic_memory.mcp.prompts import sync_status
13
14
 
14
15
  __all__ = [
15
16
  "ai_assistant_guide",
16
17
  "continue_conversation",
17
18
  "recent_activity",
18
19
  "search",
20
+ "sync_status",
19
21
  ]
@@ -0,0 +1,116 @@
1
+ """Sync status prompt for Basic Memory MCP server."""
2
+
3
+ from basic_memory.mcp.server import mcp
4
+
5
+
6
+ @mcp.prompt(
7
+ description="""Get sync status with recommendations for AI assistants.
8
+
9
+ This prompt provides both current sync status and guidance on how
10
+ AI assistants should respond when sync operations are in progress or completed.
11
+ """,
12
+ )
13
+ async def sync_status_prompt() -> str:
14
+ """Get sync status with AI assistant guidance.
15
+
16
+ This prompt provides detailed sync status information along with
17
+ recommendations for how AI assistants should handle different sync states.
18
+
19
+ Returns:
20
+ Formatted sync status with AI assistant guidance
21
+ """
22
+ try: # pragma: no cover
23
+ from basic_memory.services.migration_service import migration_manager
24
+
25
+ state = migration_manager.state
26
+
27
+ # Build status report
28
+ lines = [
29
+ "# Basic Memory Sync Status",
30
+ "",
31
+ f"**Current Status**: {state.status.value.replace('_', ' ').title()}",
32
+ f"**System Ready**: {'Yes' if migration_manager.is_ready else 'No'}",
33
+ "",
34
+ ]
35
+
36
+ if migration_manager.is_ready:
37
+ lines.extend(
38
+ [
39
+ "✅ **All sync operations completed** - System is fully operational",
40
+ "",
41
+ "All Basic Memory tools are available and functioning normally.",
42
+ "File indexing is complete and knowledge graphs are up to date.",
43
+ "You can proceed with any knowledge management tasks.",
44
+ ]
45
+ )
46
+ else:
47
+ lines.append(f"**Status Message**: {state.message}")
48
+
49
+ if state.status.value == "in_progress":
50
+ if state.projects_total > 0:
51
+ progress = f" ({state.projects_migrated}/{state.projects_total})"
52
+ lines.append(f"**Progress**: {progress}")
53
+
54
+ lines.extend(
55
+ [
56
+ "",
57
+ "🔄 **File synchronization in progress** - Processing files and building knowledge graphs",
58
+ "",
59
+ "**Impact**: Some tools may show status messages instead of normal responses",
60
+ "until sync completes (usually 1-3 minutes).",
61
+ ]
62
+ )
63
+
64
+ elif state.status.value == "failed":
65
+ lines.extend(
66
+ [
67
+ "",
68
+ f"❌ **Sync failed**: {state.error or 'Unknown error'}",
69
+ "",
70
+ "**Impact**: System may have limited functionality until issue is resolved.",
71
+ ]
72
+ )
73
+
74
+ # Add AI assistant recommendations
75
+ if not migration_manager.is_ready:
76
+ lines.extend(
77
+ [
78
+ "",
79
+ "---",
80
+ "",
81
+ "## AI Assistant Recommendations",
82
+ "",
83
+ "**When sync is in progress:**",
84
+ "- Inform the user about the background file processing",
85
+ "- Suggest using `sync_status()` tool to check progress",
86
+ "- Explain that tools will work normally once sync completes",
87
+ "- Avoid creating complex workflows until sync is done",
88
+ "",
89
+ "**What to tell users:**",
90
+ "- 'Basic Memory is processing your files and building knowledge graphs'",
91
+ "- 'This usually takes 1-3 minutes depending on your content size'",
92
+ "- 'You can check progress anytime with the sync_status tool'",
93
+ "- 'Full functionality will be available once processing completes'",
94
+ "",
95
+ "**User-friendly language:**",
96
+ "- Say 'processing files' instead of 'migration' or 'sync'",
97
+ "- Say 'building knowledge graphs' instead of 'indexing'",
98
+ "- Say 'setting up your knowledge base' instead of 'running migrations'",
99
+ ]
100
+ )
101
+
102
+ return "\n".join(lines)
103
+
104
+ except Exception as e: # pragma: no cover
105
+ return f"""# Sync Status - Error
106
+
107
+ ❌ **Unable to check sync status**: {str(e)}
108
+
109
+ ## AI Assistant Recommendations
110
+
111
+ **When status is unavailable:**
112
+ - Assume the system is likely working normally
113
+ - Try proceeding with normal operations
114
+ - If users report issues, suggest checking logs or restarting
115
+ - Use user-friendly language about 'setting up the knowledge base'
116
+ """
@@ -31,23 +31,23 @@ load_dotenv()
31
31
  @dataclass
32
32
  class AppContext:
33
33
  watch_task: Optional[asyncio.Task]
34
+ migration_manager: Optional[Any] = None
34
35
 
35
36
 
36
37
  @asynccontextmanager
37
38
  async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: # pragma: no cover
38
39
  """Manage application lifecycle with type-safe context"""
39
- # Initialize on startup
40
- watch_task = await initialize_app(app_config)
40
+ # Initialize on startup (now returns migration_manager)
41
+ migration_manager = await initialize_app(app_config)
41
42
 
42
43
  # Initialize project session with default project
43
44
  session.initialize(app_config.default_project)
44
45
 
45
46
  try:
46
- yield AppContext(watch_task=watch_task)
47
+ yield AppContext(watch_task=None, migration_manager=migration_manager)
47
48
  finally:
48
- # Cleanup on shutdown
49
- if watch_task:
50
- watch_task.cancel()
49
+ # Cleanup on shutdown - migration tasks will be cancelled automatically
50
+ pass
51
51
 
52
52
 
53
53
  # OAuth configuration function
@@ -11,12 +11,14 @@ from basic_memory.mcp.tools.read_content import read_content
11
11
  from basic_memory.mcp.tools.build_context import build_context
12
12
  from basic_memory.mcp.tools.recent_activity import recent_activity
13
13
  from basic_memory.mcp.tools.read_note import read_note
14
+ from basic_memory.mcp.tools.view_note import view_note
14
15
  from basic_memory.mcp.tools.write_note import write_note
15
16
  from basic_memory.mcp.tools.search import search_notes
16
17
  from basic_memory.mcp.tools.canvas import canvas
17
18
  from basic_memory.mcp.tools.list_directory import list_directory
18
19
  from basic_memory.mcp.tools.edit_note import edit_note
19
20
  from basic_memory.mcp.tools.move_note import move_note
21
+ from basic_memory.mcp.tools.sync_status import sync_status
20
22
  from basic_memory.mcp.tools.project_management import (
21
23
  list_projects,
22
24
  switch_project,
@@ -43,5 +45,7 @@ __all__ = [
43
45
  "search_notes",
44
46
  "set_default_project",
45
47
  "switch_project",
48
+ "sync_status",
49
+ "view_note",
46
50
  "write_note",
47
51
  ]
@@ -13,7 +13,6 @@ from basic_memory.schemas.memory import (
13
13
  GraphContext,
14
14
  MemoryUrl,
15
15
  memory_url_path,
16
- normalize_memory_url,
17
16
  )
18
17
 
19
18
 
@@ -21,12 +20,17 @@ from basic_memory.schemas.memory import (
21
20
  description="""Build context from a memory:// URI to continue conversations naturally.
22
21
 
23
22
  Use this to follow up on previous discussions or explore related topics.
23
+
24
+ Memory URL Format:
25
+ - Use paths like "folder/note" or "memory://folder/note"
26
+ - Pattern matching: "folder/*" matches all notes in folder
27
+ - Valid characters: letters, numbers, hyphens, underscores, forward slashes
28
+ - Avoid: double slashes (//), angle brackets (<>), quotes, pipes (|)
29
+ - Examples: "specs/search", "projects/basic-memory", "notes/*"
30
+
24
31
  Timeframes support natural language like:
25
- - "2 days ago"
26
- - "last week"
27
- - "today"
28
- - "3 months ago"
29
- Or standard formats like "7d", "24h"
32
+ - "2 days ago", "last week", "today", "3 months ago"
33
+ - Or standard formats like "7d", "24h"
30
34
  """,
31
35
  )
32
36
  async def build_context(
@@ -76,7 +80,28 @@ async def build_context(
76
80
  build_context("memory://specs/search", project="work-project")
77
81
  """
78
82
  logger.info(f"Building context from {url}")
79
- url = normalize_memory_url(url)
83
+ # URL is already validated and normalized by MemoryUrl type annotation
84
+
85
+ # Check migration status and wait briefly if needed
86
+ from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
87
+
88
+ migration_status = await wait_for_migration_or_return_status(timeout=5.0)
89
+ if migration_status: # pragma: no cover
90
+ # Return a proper GraphContext with status message
91
+ from basic_memory.schemas.memory import MemoryMetadata
92
+ from datetime import datetime
93
+
94
+ return GraphContext(
95
+ results=[],
96
+ metadata=MemoryMetadata(
97
+ depth=depth or 1,
98
+ timeframe=timeframe,
99
+ generated_at=datetime.now(),
100
+ primary_count=0,
101
+ related_count=0,
102
+ uri=migration_status, # Include status in metadata
103
+ ),
104
+ )
80
105
 
81
106
  active_project = get_active_project(project)
82
107
  project_url = active_project.project_url
@@ -35,7 +35,8 @@ async def canvas(
35
35
  nodes: List of node objects following JSON Canvas 1.0 spec
36
36
  edges: List of edge objects following JSON Canvas 1.0 spec
37
37
  title: The title of the canvas (will be saved as title.canvas)
38
- folder: The folder where the file should be saved
38
+ folder: Folder path relative to project root where the canvas should be saved.
39
+ Use forward slashes (/) as separators. Examples: "diagrams", "projects/2025", "visual/maps"
39
40
  project: Optional project name to create canvas in. If not provided, uses current active project.
40
41
 
41
42
  Returns:
@@ -1,5 +1,8 @@
1
+ from textwrap import dedent
1
2
  from typing import Optional
2
3
 
4
+ from loguru import logger
5
+
3
6
  from basic_memory.mcp.tools.utils import call_delete
4
7
  from basic_memory.mcp.server import mcp
5
8
  from basic_memory.mcp.async_client import client
@@ -7,8 +10,148 @@ from basic_memory.mcp.project_session import get_active_project
7
10
  from basic_memory.schemas import DeleteEntitiesResponse
8
11
 
9
12
 
13
+ def _format_delete_error_response(error_message: str, identifier: str) -> str:
14
+ """Format helpful error responses for delete failures that guide users to successful deletions."""
15
+
16
+ # Note not found errors
17
+ if "entity not found" in error_message.lower() or "not found" in error_message.lower():
18
+ search_term = identifier.split("/")[-1] if "/" in identifier else identifier
19
+ title_format = (
20
+ identifier.split("/")[-1].replace("-", " ").title() if "/" in identifier else identifier
21
+ )
22
+ permalink_format = identifier.lower().replace(" ", "-")
23
+
24
+ return dedent(f"""
25
+ # Delete Failed - Note Not Found
26
+
27
+ The note '{identifier}' could not be found for deletion.
28
+
29
+ ## This might mean:
30
+ 1. **Already deleted**: The note may have been deleted previously
31
+ 2. **Wrong identifier**: The identifier format might be incorrect
32
+ 3. **Different project**: The note might be in a different project
33
+
34
+ ## How to verify:
35
+ 1. **Search for the note**: Use `search_notes("{search_term}")` to find it
36
+ 2. **Try different formats**:
37
+ - If you used a permalink like "folder/note-title", try just the title: "{title_format}"
38
+ - If you used a title, try the permalink format: "{permalink_format}"
39
+
40
+ 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
+
43
+ ## If the note actually exists:
44
+ ```
45
+ # First, find the correct identifier:
46
+ search_notes("{identifier}")
47
+
48
+ # Then delete using the correct identifier:
49
+ delete_note("correct-identifier-from-search")
50
+ ```
51
+
52
+ ## If you want to delete multiple similar notes:
53
+ Use search to find all related notes and delete them one by one.
54
+ """).strip()
55
+
56
+ # Permission/access errors
57
+ if (
58
+ "permission" in error_message.lower()
59
+ or "access" in error_message.lower()
60
+ or "forbidden" in error_message.lower()
61
+ ):
62
+ return f"""# Delete Failed - Permission Error
63
+
64
+ You don't have permission to delete '{identifier}': {error_message}
65
+
66
+ ## How to resolve:
67
+ 1. **Check permissions**: Verify you have delete/write access to this project
68
+ 2. **File locks**: The note might be open in another application
69
+ 3. **Project access**: Ensure you're in the correct project with proper permissions
70
+
71
+ ## 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}")`
75
+
76
+ ## 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
+
79
+ # Server/filesystem errors
80
+ if (
81
+ "server error" in error_message.lower()
82
+ or "filesystem" in error_message.lower()
83
+ or "disk" in error_message.lower()
84
+ ):
85
+ return f"""# Delete Failed - System Error
86
+
87
+ A system error occurred while deleting '{identifier}': {error_message}
88
+
89
+ ## Immediate steps:
90
+ 1. **Try again**: The error might be temporary
91
+ 2. **Check file status**: Verify the file isn't locked or in use
92
+ 3. **Check disk space**: Ensure the system has adequate storage
93
+
94
+ ## Troubleshooting:
95
+ - Verify note exists: `read_note("{identifier}")`
96
+ - Check project status: `get_current_project()`
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("{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("{identifier}")` or `search_notes("{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("{identifier}")
136
+
137
+ # 2. Read the note to verify access
138
+ read_note("correct-identifier-from-search")
139
+
140
+ # 3. Try deletion with correct identifier
141
+ delete_note("correct-identifier-from-search")
142
+ ```
143
+
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")`
148
+
149
+ ## Need help?
150
+ If the note should be deleted but the operation keeps failing, send a message to support@basicmachines.co."""
151
+
152
+
10
153
  @mcp.tool(description="Delete a note by title or permalink")
11
- async def delete_note(identifier: str, project: Optional[str] = None) -> bool:
154
+ async def delete_note(identifier: str, project: Optional[str] = None) -> bool | str:
12
155
  """Delete a note from the knowledge base.
13
156
 
14
157
  Args:
@@ -31,6 +174,18 @@ async def delete_note(identifier: str, project: Optional[str] = None) -> bool:
31
174
  active_project = get_active_project(project)
32
175
  project_url = active_project.project_url
33
176
 
34
- response = await call_delete(client, f"{project_url}/knowledge/entities/{identifier}")
35
- result = DeleteEntitiesResponse.model_validate(response.json())
36
- return result.deleted
177
+ try:
178
+ response = await call_delete(client, f"{project_url}/knowledge/entities/{identifier}")
179
+ result = DeleteEntitiesResponse.model_validate(response.json())
180
+
181
+ if result.deleted:
182
+ logger.info(f"Successfully deleted note: {identifier}")
183
+ return True
184
+ else:
185
+ logger.warning(f"Delete operation completed but note was not deleted: {identifier}")
186
+ return False
187
+
188
+ except Exception as e: # pragma: no cover
189
+ logger.error(f"Delete failed for '{identifier}': {e}")
190
+ # Return formatted error message for better user experience
191
+ return _format_delete_error_response(str(e), identifier)
@@ -24,14 +24,14 @@ def _format_error_response(
24
24
  if "Entity not found" in error_message or "entity not found" in error_message.lower():
25
25
  return f"""# Edit Failed - Note Not Found
26
26
 
27
- The note with identifier '{identifier}' could not be found.
27
+ The note with identifier '{identifier}' could not be found. Edit operations require an exact match (no fuzzy matching).
28
28
 
29
29
  ## Suggestions to try:
30
- 1. **Search for the note first**: Use `search_notes("{identifier.split("/")[-1]}")` to find similar notes
31
- 2. **Try different identifier formats**:
32
- - If you used a permalink like "folder/note-title", try just the title: "{identifier.split("/")[-1].replace("-", " ").title()}"
33
- - If you used a title, try the permalink format: "{identifier.lower().replace(" ", "-")}"
34
- - Use `read_note()` first to verify the note exists and get the correct identifiers
30
+ 1. **Search for the note first**: Use `search_notes("{identifier.split("/")[-1]}")` to find similar notes with exact identifiers
31
+ 2. **Try different exact identifier formats**:
32
+ - If you used a permalink like "folder/note-title", try the exact title: "{identifier.split("/")[-1].replace("-", " ").title()}"
33
+ - If you used a title, try the exact permalink format: "{identifier.lower().replace(" ", "-")}"
34
+ - Use `read_note()` first to verify the note exists and get the exact identifier
35
35
 
36
36
  ## Alternative approach:
37
37
  Use `write_note()` to create the note first, then edit it."""
@@ -142,7 +142,9 @@ async def edit_note(
142
142
  It supports various operations for different editing scenarios.
143
143
 
144
144
  Args:
145
- identifier: The title, permalink, or memory:// URL of the note to edit
145
+ identifier: The exact title, permalink, or memory:// URL of the note to edit.
146
+ Must be an exact match - fuzzy matching is not supported for edit operations.
147
+ Use search_notes() or read_note() first to find the correct identifier if uncertain.
146
148
  operation: The editing operation to perform:
147
149
  - "append": Add content to the end of the note
148
150
  - "prepend": Add content to the beginning of the note
@@ -179,10 +181,14 @@ async def edit_note(
179
181
  # Replace subsection with more specific header
180
182
  edit_note("docs/setup", "replace_section", "Updated install steps\\n", section="### Installation")
181
183
 
182
- # Using different identifier formats
183
- edit_note("Meeting Notes", "append", "\\n- Follow up on action items") # title
184
- edit_note("docs/meeting-notes", "append", "\\n- Follow up tasks") # permalink
185
- edit_note("docs/Meeting Notes", "append", "\\n- Next steps") # folder/title
184
+ # Using different identifier formats (must be exact matches)
185
+ edit_note("Meeting Notes", "append", "\\n- Follow up on action items") # exact title
186
+ edit_note("docs/meeting-notes", "append", "\\n- Follow up tasks") # exact permalink
187
+ edit_note("docs/Meeting Notes", "append", "\\n- Next steps") # exact folder/title
188
+
189
+ # If uncertain about identifier, search first:
190
+ # search_notes("meeting") # Find available notes
191
+ # edit_note("docs/meeting-notes-2025", "append", "content") # Use exact result
186
192
 
187
193
  # Add new section to document
188
194
  edit_note("project-plan", "replace_section", "TBD - needs research\\n", section="## Future Work")