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.
- basic_memory/__init__.py +1 -7
- basic_memory/api/routers/knowledge_router.py +13 -0
- basic_memory/api/routers/memory_router.py +3 -4
- basic_memory/api/routers/project_router.py +6 -5
- basic_memory/api/routers/prompt_router.py +2 -2
- basic_memory/cli/commands/project.py +2 -2
- basic_memory/cli/commands/status.py +1 -1
- basic_memory/cli/commands/sync.py +1 -1
- basic_memory/mcp/prompts/__init__.py +2 -0
- basic_memory/mcp/prompts/sync_status.py +116 -0
- basic_memory/mcp/server.py +6 -6
- basic_memory/mcp/tools/__init__.py +4 -0
- basic_memory/mcp/tools/build_context.py +32 -7
- basic_memory/mcp/tools/canvas.py +2 -1
- basic_memory/mcp/tools/delete_note.py +159 -4
- basic_memory/mcp/tools/edit_note.py +17 -11
- basic_memory/mcp/tools/move_note.py +252 -40
- basic_memory/mcp/tools/project_management.py +35 -3
- basic_memory/mcp/tools/read_note.py +9 -2
- basic_memory/mcp/tools/search.py +180 -8
- basic_memory/mcp/tools/sync_status.py +254 -0
- basic_memory/mcp/tools/utils.py +47 -0
- basic_memory/mcp/tools/view_note.py +66 -0
- basic_memory/mcp/tools/write_note.py +13 -2
- basic_memory/repository/search_repository.py +99 -26
- basic_memory/schemas/base.py +33 -5
- basic_memory/schemas/memory.py +58 -1
- basic_memory/services/entity_service.py +4 -4
- basic_memory/services/initialization.py +32 -5
- basic_memory/services/link_resolver.py +20 -5
- basic_memory/services/migration_service.py +168 -0
- basic_memory/services/project_service.py +97 -47
- basic_memory/services/sync_status_service.py +181 -0
- basic_memory/sync/sync_service.py +55 -2
- {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/METADATA +2 -2
- {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/RECORD +39 -34
- {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/WHEEL +0 -0
- {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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=
|
|
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 =
|
|
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
|
+
"""
|
basic_memory/mcp/server.py
CHANGED
|
@@ -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
|
-
|
|
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=
|
|
47
|
+
yield AppContext(watch_task=None, migration_manager=migration_manager)
|
|
47
48
|
finally:
|
|
48
|
-
# Cleanup on shutdown
|
|
49
|
-
|
|
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
|
-
- "
|
|
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
|
-
|
|
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
|
basic_memory/mcp/tools/canvas.py
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
|
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")
|