basic-memory 0.15.0__py3-none-any.whl → 0.15.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.
- basic_memory/__init__.py +1 -1
- basic_memory/api/routers/directory_router.py +23 -2
- basic_memory/api/routers/project_router.py +1 -0
- basic_memory/cli/auth.py +2 -2
- basic_memory/cli/commands/command_utils.py +11 -28
- basic_memory/cli/commands/mcp.py +72 -67
- basic_memory/cli/commands/project.py +54 -49
- basic_memory/cli/commands/status.py +6 -15
- basic_memory/config.py +55 -9
- basic_memory/deps.py +7 -5
- basic_memory/ignore_utils.py +7 -7
- basic_memory/mcp/async_client.py +102 -4
- basic_memory/mcp/prompts/continue_conversation.py +16 -15
- basic_memory/mcp/prompts/search.py +12 -11
- basic_memory/mcp/resources/ai_assistant_guide.md +185 -453
- basic_memory/mcp/resources/project_info.py +9 -7
- basic_memory/mcp/tools/build_context.py +40 -39
- basic_memory/mcp/tools/canvas.py +21 -20
- basic_memory/mcp/tools/chatgpt_tools.py +11 -2
- basic_memory/mcp/tools/delete_note.py +22 -21
- basic_memory/mcp/tools/edit_note.py +105 -104
- basic_memory/mcp/tools/list_directory.py +98 -95
- basic_memory/mcp/tools/move_note.py +127 -125
- basic_memory/mcp/tools/project_management.py +101 -98
- basic_memory/mcp/tools/read_content.py +64 -63
- basic_memory/mcp/tools/read_note.py +88 -88
- basic_memory/mcp/tools/recent_activity.py +139 -135
- basic_memory/mcp/tools/search.py +27 -26
- basic_memory/mcp/tools/sync_status.py +133 -128
- basic_memory/mcp/tools/utils.py +0 -15
- basic_memory/mcp/tools/view_note.py +14 -28
- basic_memory/mcp/tools/write_note.py +97 -87
- basic_memory/repository/entity_repository.py +60 -0
- basic_memory/repository/repository.py +16 -3
- basic_memory/repository/search_repository.py +42 -0
- basic_memory/schemas/project_info.py +1 -1
- basic_memory/services/directory_service.py +124 -3
- basic_memory/services/entity_service.py +31 -9
- basic_memory/services/project_service.py +97 -10
- basic_memory/services/search_service.py +16 -8
- basic_memory/sync/sync_service.py +28 -13
- {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/METADATA +51 -4
- {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/RECORD +46 -47
- basic_memory/mcp/tools/headers.py +0 -44
- {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/WHEEL +0 -0
- {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,7 +6,7 @@ from loguru import logger
|
|
|
6
6
|
from fastmcp import Context
|
|
7
7
|
|
|
8
8
|
from basic_memory.config import ConfigManager
|
|
9
|
-
from basic_memory.mcp.async_client import
|
|
9
|
+
from basic_memory.mcp.async_client import get_client
|
|
10
10
|
from basic_memory.mcp.server import mcp
|
|
11
11
|
from basic_memory.mcp.project_context import get_active_project
|
|
12
12
|
from basic_memory.services.sync_status_service import sync_status_tracker
|
|
@@ -95,162 +95,167 @@ async def sync_status(project: Optional[str] = None, context: Context | None = N
|
|
|
95
95
|
"""
|
|
96
96
|
logger.info("MCP tool call tool=sync_status")
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
async with get_client() as client:
|
|
99
|
+
status_lines = []
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
status_lines.extend(
|
|
109
|
-
[
|
|
110
|
-
"# Basic Memory Sync Status",
|
|
111
|
-
"",
|
|
112
|
-
f"**Current Status**: {summary}",
|
|
113
|
-
f"**System Ready**: {'✅ Yes' if is_ready else '🔄 Processing'}",
|
|
114
|
-
"",
|
|
115
|
-
]
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
if is_ready:
|
|
101
|
+
try:
|
|
102
|
+
from basic_memory.services.sync_status_service import sync_status_tracker
|
|
103
|
+
|
|
104
|
+
# Get overall summary
|
|
105
|
+
summary = sync_status_tracker.get_summary()
|
|
106
|
+
is_ready = sync_status_tracker.is_ready
|
|
107
|
+
|
|
108
|
+
# Header
|
|
119
109
|
status_lines.extend(
|
|
120
110
|
[
|
|
121
|
-
"
|
|
111
|
+
"# Basic Memory Sync Status",
|
|
122
112
|
"",
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"- All Basic Memory tools are fully operational",
|
|
113
|
+
f"**Current Status**: {summary}",
|
|
114
|
+
f"**System Ready**: {'✅ Yes' if is_ready else '🔄 Processing'}",
|
|
126
115
|
"",
|
|
127
|
-
"Your knowledge base is ready for use!",
|
|
128
116
|
]
|
|
129
117
|
)
|
|
130
118
|
|
|
131
|
-
|
|
132
|
-
status_lines.extend(_get_all_projects_status())
|
|
133
|
-
else:
|
|
134
|
-
# System is still processing - show both active and all projects
|
|
135
|
-
all_sync_projects = sync_status_tracker.get_all_projects()
|
|
136
|
-
|
|
137
|
-
active_projects = [
|
|
138
|
-
p for p in all_sync_projects.values() if p.status.value in ["scanning", "syncing"]
|
|
139
|
-
]
|
|
140
|
-
failed_projects = [p for p in all_sync_projects.values() if p.status.value == "failed"]
|
|
141
|
-
|
|
142
|
-
if active_projects:
|
|
119
|
+
if is_ready:
|
|
143
120
|
status_lines.extend(
|
|
144
121
|
[
|
|
145
|
-
"
|
|
122
|
+
"✅ **All sync operations completed**",
|
|
146
123
|
"",
|
|
147
|
-
"
|
|
148
|
-
"
|
|
124
|
+
"- File indexing is complete",
|
|
125
|
+
"- Knowledge graphs are up to date",
|
|
126
|
+
"- All Basic Memory tools are fully operational",
|
|
149
127
|
"",
|
|
150
|
-
"
|
|
128
|
+
"Your knowledge base is ready for use!",
|
|
151
129
|
]
|
|
152
130
|
)
|
|
153
131
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
) * 100
|
|
160
|
-
progress = f" ({project_status.files_processed}/{project_status.files_total}, {progress_pct:.0f}%)"
|
|
132
|
+
# Show all projects status even when ready
|
|
133
|
+
status_lines.extend(_get_all_projects_status())
|
|
134
|
+
else:
|
|
135
|
+
# System is still processing - show both active and all projects
|
|
136
|
+
all_sync_projects = sync_status_tracker.get_all_projects()
|
|
161
137
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
"- Scanning and indexing markdown files",
|
|
171
|
-
"- Building entity and relationship graphs",
|
|
172
|
-
"- Setting up full-text search indexes",
|
|
173
|
-
"- Processing file changes and updates",
|
|
174
|
-
"",
|
|
175
|
-
"**What you can do:**",
|
|
176
|
-
"- Wait for automatic processing to complete - no action needed",
|
|
177
|
-
"- Use this tool again to check progress",
|
|
178
|
-
"- Simple operations may work already",
|
|
179
|
-
"- All projects will be available once sync finishes",
|
|
180
|
-
]
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
# Handle failed projects (independent of active projects)
|
|
184
|
-
if failed_projects:
|
|
185
|
-
status_lines.extend(["", "❌ **Some projects failed to sync:**", ""])
|
|
138
|
+
active_projects = [
|
|
139
|
+
p
|
|
140
|
+
for p in all_sync_projects.values()
|
|
141
|
+
if p.status.value in ["scanning", "syncing"]
|
|
142
|
+
]
|
|
143
|
+
failed_projects = [
|
|
144
|
+
p for p in all_sync_projects.values() if p.status.value == "failed"
|
|
145
|
+
]
|
|
186
146
|
|
|
187
|
-
|
|
188
|
-
status_lines.
|
|
189
|
-
|
|
147
|
+
if active_projects:
|
|
148
|
+
status_lines.extend(
|
|
149
|
+
[
|
|
150
|
+
"🔄 **File synchronization in progress**",
|
|
151
|
+
"",
|
|
152
|
+
"Basic Memory is automatically processing all configured projects and building knowledge graphs.",
|
|
153
|
+
"This typically takes 1-3 minutes depending on the amount of content.",
|
|
154
|
+
"",
|
|
155
|
+
"**Currently Processing:**",
|
|
156
|
+
]
|
|
190
157
|
)
|
|
191
158
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
159
|
+
for project_status in active_projects:
|
|
160
|
+
progress = ""
|
|
161
|
+
if project_status.files_total > 0:
|
|
162
|
+
progress_pct = (
|
|
163
|
+
project_status.files_processed / project_status.files_total
|
|
164
|
+
) * 100
|
|
165
|
+
progress = f" ({project_status.files_processed}/{project_status.files_total}, {progress_pct:.0f}%)"
|
|
166
|
+
|
|
167
|
+
status_lines.append(
|
|
168
|
+
f"- **{project_status.project_name}**: {project_status.message}{progress}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
status_lines.extend(
|
|
172
|
+
[
|
|
173
|
+
"",
|
|
174
|
+
"**What's happening:**",
|
|
175
|
+
"- Scanning and indexing markdown files",
|
|
176
|
+
"- Building entity and relationship graphs",
|
|
177
|
+
"- Settings up full-text search indexes",
|
|
178
|
+
"- Processing file changes and updates",
|
|
179
|
+
"",
|
|
180
|
+
"**What you can do:**",
|
|
181
|
+
"- Wait for automatic processing to complete - no action needed",
|
|
182
|
+
"- Use this tool again to check progress",
|
|
183
|
+
"- Simple operations may work already",
|
|
184
|
+
"- All projects will be available once sync finishes",
|
|
185
|
+
]
|
|
186
|
+
)
|
|
212
187
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
188
|
+
# Handle failed projects (independent of active projects)
|
|
189
|
+
if failed_projects:
|
|
190
|
+
status_lines.extend(["", "❌ **Some projects failed to sync:**", ""])
|
|
191
|
+
|
|
192
|
+
for project_status in failed_projects:
|
|
193
|
+
status_lines.append(
|
|
194
|
+
f"- **{project_status.project_name}**: {project_status.error or 'Unknown error'}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
status_lines.extend(
|
|
198
|
+
[
|
|
199
|
+
"",
|
|
200
|
+
"**Next steps:**",
|
|
201
|
+
"1. Check the logs for detailed error information",
|
|
202
|
+
"2. Ensure file permissions allow read/write access",
|
|
203
|
+
"3. Try restarting the MCP server",
|
|
204
|
+
"4. If issues persist, consider filing a support issue",
|
|
205
|
+
]
|
|
206
|
+
)
|
|
207
|
+
elif not active_projects:
|
|
208
|
+
# No active or failed projects - must be pending
|
|
209
|
+
status_lines.extend(
|
|
210
|
+
[
|
|
211
|
+
"⏳ **Sync operations pending**",
|
|
212
|
+
"",
|
|
213
|
+
"File synchronization has been queued but hasn't started yet.",
|
|
214
|
+
"This usually resolves automatically within a few seconds.",
|
|
215
|
+
]
|
|
216
|
+
)
|
|
217
217
|
|
|
218
|
-
# Add
|
|
219
|
-
|
|
220
|
-
if
|
|
221
|
-
status_lines.extend(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
218
|
+
# Add comprehensive project status for all configured projects
|
|
219
|
+
all_projects_status = _get_all_projects_status()
|
|
220
|
+
if all_projects_status:
|
|
221
|
+
status_lines.extend(all_projects_status)
|
|
222
|
+
|
|
223
|
+
# Add explanation about automatic syncing if there are unsynced projects
|
|
224
|
+
unsynced_count = sum(1 for line in all_projects_status if "⏳" in line)
|
|
225
|
+
if unsynced_count > 0 and not is_ready:
|
|
226
|
+
status_lines.extend(
|
|
227
|
+
[
|
|
228
|
+
"",
|
|
229
|
+
"**Note**: All configured projects will be automatically synced during startup.",
|
|
230
|
+
]
|
|
231
|
+
)
|
|
227
232
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
233
|
+
# Add project context if provided
|
|
234
|
+
if project:
|
|
235
|
+
try:
|
|
236
|
+
active_project = await get_active_project(client, project, context)
|
|
237
|
+
status_lines.extend(
|
|
238
|
+
[
|
|
239
|
+
"",
|
|
240
|
+
"---",
|
|
241
|
+
"",
|
|
242
|
+
f"**Active Project**: {active_project.name}",
|
|
243
|
+
f"**Project Path**: {active_project.home}",
|
|
244
|
+
]
|
|
245
|
+
)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.debug(f"Could not get project info: {e}")
|
|
243
248
|
|
|
244
|
-
|
|
249
|
+
return "\n".join(status_lines)
|
|
245
250
|
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
except Exception as e:
|
|
252
|
+
return f"""# Sync Status - Error
|
|
248
253
|
|
|
249
254
|
❌ **Unable to check sync status**: {str(e)}
|
|
250
255
|
|
|
251
256
|
**Troubleshooting:**
|
|
252
257
|
- The system may still be starting up
|
|
253
|
-
- Try waiting a few seconds and checking again
|
|
258
|
+
- Try waiting a few seconds and checking again
|
|
254
259
|
- Check logs for detailed error information
|
|
255
260
|
- Consider restarting if the issue persists
|
|
256
261
|
"""
|
basic_memory/mcp/tools/utils.py
CHANGED
|
@@ -23,8 +23,6 @@ from httpx._types import (
|
|
|
23
23
|
from loguru import logger
|
|
24
24
|
from mcp.server.fastmcp.exceptions import ToolError
|
|
25
25
|
|
|
26
|
-
from basic_memory.mcp.tools.headers import inject_auth_header
|
|
27
|
-
|
|
28
26
|
|
|
29
27
|
def get_error_message(
|
|
30
28
|
status_code: int, url: URL | str, method: str, msg: Optional[str] = None
|
|
@@ -110,7 +108,6 @@ async def call_get(
|
|
|
110
108
|
logger.debug(f"Calling GET '{url}' params: '{params}'")
|
|
111
109
|
error_message = None
|
|
112
110
|
|
|
113
|
-
headers = inject_auth_header(headers)
|
|
114
111
|
try:
|
|
115
112
|
response = await client.get(
|
|
116
113
|
url,
|
|
@@ -196,9 +193,6 @@ async def call_put(
|
|
|
196
193
|
logger.debug(f"Calling PUT '{url}'")
|
|
197
194
|
error_message = None
|
|
198
195
|
|
|
199
|
-
# Inject JWT from FastMCP context if available
|
|
200
|
-
headers = inject_auth_header(headers)
|
|
201
|
-
|
|
202
196
|
try:
|
|
203
197
|
response = await client.put(
|
|
204
198
|
url,
|
|
@@ -288,9 +282,6 @@ async def call_patch(
|
|
|
288
282
|
"""
|
|
289
283
|
logger.debug(f"Calling PATCH '{url}'")
|
|
290
284
|
|
|
291
|
-
# Inject JWT from FastMCP context if available
|
|
292
|
-
headers = inject_auth_header(headers)
|
|
293
|
-
|
|
294
285
|
try:
|
|
295
286
|
response = await client.patch(
|
|
296
287
|
url,
|
|
@@ -396,9 +387,6 @@ async def call_post(
|
|
|
396
387
|
logger.debug(f"Calling POST '{url}'")
|
|
397
388
|
error_message = None
|
|
398
389
|
|
|
399
|
-
# Inject JWT from FastMCP context if available
|
|
400
|
-
headers = inject_auth_header(headers)
|
|
401
|
-
|
|
402
390
|
try:
|
|
403
391
|
response = await client.post(
|
|
404
392
|
url=url,
|
|
@@ -481,9 +469,6 @@ async def call_delete(
|
|
|
481
469
|
logger.debug(f"Calling DELETE '{url}'")
|
|
482
470
|
error_message = None
|
|
483
471
|
|
|
484
|
-
# Inject JWT from FastMCP context if available
|
|
485
|
-
headers = inject_auth_header(headers)
|
|
486
|
-
|
|
487
472
|
try:
|
|
488
473
|
response = await client.delete(
|
|
489
474
|
url=url,
|
|
@@ -22,14 +22,10 @@ async def view_note(
|
|
|
22
22
|
) -> str:
|
|
23
23
|
"""View a markdown note as a formatted artifact.
|
|
24
24
|
|
|
25
|
-
This tool reads a note using the same logic as read_note but
|
|
26
|
-
as a markdown artifact
|
|
25
|
+
This tool reads a note using the same logic as read_note but instructs Claude
|
|
26
|
+
to display the content as a markdown artifact in the Claude Desktop app.
|
|
27
27
|
Project parameter optional with server resolution.
|
|
28
28
|
|
|
29
|
-
After calling this tool, create an artifact using the returned content to display
|
|
30
|
-
the note in a readable format. The tool returns the note content that should be
|
|
31
|
-
used to create a markdown artifact.
|
|
32
|
-
|
|
33
29
|
Args:
|
|
34
30
|
identifier: The title or permalink of the note to view
|
|
35
31
|
project: Project name to read from. Optional - server will resolve using hierarchy.
|
|
@@ -39,7 +35,7 @@ async def view_note(
|
|
|
39
35
|
context: Optional FastMCP context for performance caching.
|
|
40
36
|
|
|
41
37
|
Returns:
|
|
42
|
-
|
|
38
|
+
Instructions for Claude to create a markdown artifact with the note content.
|
|
43
39
|
|
|
44
40
|
Examples:
|
|
45
41
|
# View a note by title
|
|
@@ -66,26 +62,16 @@ async def view_note(
|
|
|
66
62
|
|
|
67
63
|
# Check if this is an error message (note not found)
|
|
68
64
|
if "# Note Not Found" in content:
|
|
69
|
-
return content # Return error message directly
|
|
70
|
-
|
|
71
|
-
# Extract title from content if possible
|
|
72
|
-
title = identifier
|
|
73
|
-
lines = content.split("\n")
|
|
74
|
-
for line in lines:
|
|
75
|
-
if line.startswith("title:"):
|
|
76
|
-
# Extract title from frontmatter
|
|
77
|
-
title = line.split("title:", 1)[1].strip().strip('"').strip("'")
|
|
78
|
-
break
|
|
79
|
-
elif line.startswith("# "):
|
|
80
|
-
# Extract title from first heading
|
|
81
|
-
title = line[2:].strip()
|
|
82
|
-
break
|
|
83
|
-
|
|
84
|
-
# Return the artifact XML as part of the response
|
|
85
|
-
artifact = f'<artifact identifier="note-{hash(identifier) & 0x7FFFFFFF}" type="text/markdown" title="{title}">\n{content}\n</artifact>'
|
|
65
|
+
return content # Return error message directly
|
|
86
66
|
|
|
67
|
+
# Return instructions for Claude to create an artifact
|
|
87
68
|
return dedent(f"""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
69
|
+
Note retrieved: "{identifier}"
|
|
70
|
+
|
|
71
|
+
Display this note as a markdown artifact for the user.
|
|
72
|
+
|
|
73
|
+
Content:
|
|
74
|
+
---
|
|
75
|
+
{content}
|
|
76
|
+
---
|
|
77
|
+
""").strip()
|
|
@@ -4,7 +4,7 @@ from typing import List, Union, Optional
|
|
|
4
4
|
|
|
5
5
|
from loguru import logger
|
|
6
6
|
|
|
7
|
-
from basic_memory.mcp.async_client import
|
|
7
|
+
from basic_memory.mcp.async_client import get_client
|
|
8
8
|
from basic_memory.mcp.project_context import get_active_project, add_project_metadata
|
|
9
9
|
from basic_memory.mcp.server import mcp
|
|
10
10
|
from basic_memory.mcp.tools.utils import call_put
|
|
@@ -63,7 +63,8 @@ async def write_note(
|
|
|
63
63
|
title: The title of the note
|
|
64
64
|
content: Markdown content for the note, can include observations and relations
|
|
65
65
|
folder: Folder path relative to project root where the file should be saved.
|
|
66
|
-
Use forward slashes (/) as separators.
|
|
66
|
+
Use forward slashes (/) as separators. Use "/" or "" to write to project root.
|
|
67
|
+
Examples: "notes", "projects/2025", "research/ml", "/" (root)
|
|
67
68
|
project: Project name to write to. Optional - server will resolve using the
|
|
68
69
|
hierarchy above. If unknown, use list_memory_projects() to discover
|
|
69
70
|
available projects.
|
|
@@ -117,92 +118,101 @@ async def write_note(
|
|
|
117
118
|
HTTPError: If project doesn't exist or is inaccessible
|
|
118
119
|
SecurityError: If folder path attempts path traversal
|
|
119
120
|
"""
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
# Get and validate the project (supports optional project parameter)
|
|
125
|
-
active_project = await get_active_project(client, project, context)
|
|
126
|
-
|
|
127
|
-
# Validate folder path to prevent path traversal attacks
|
|
128
|
-
project_path = active_project.home
|
|
129
|
-
if folder and not validate_project_path(folder, project_path):
|
|
130
|
-
logger.warning(
|
|
131
|
-
"Attempted path traversal attack blocked", folder=folder, project=active_project.name
|
|
121
|
+
async with get_client() as client:
|
|
122
|
+
logger.info(
|
|
123
|
+
f"MCP tool call tool=write_note project={project} folder={folder}, title={title}, tags={tags}"
|
|
132
124
|
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
entity = Entity(
|
|
149
|
-
title=title,
|
|
150
|
-
folder=folder,
|
|
151
|
-
entity_type=entity_type,
|
|
152
|
-
content_type="text/markdown",
|
|
153
|
-
content=content,
|
|
154
|
-
entity_metadata=metadata,
|
|
155
|
-
)
|
|
156
|
-
project_url = active_project.permalink
|
|
157
|
-
|
|
158
|
-
# Create or update via knowledge API
|
|
159
|
-
logger.debug(f"Creating entity via API permalink={entity.permalink}")
|
|
160
|
-
url = f"{project_url}/knowledge/entities/{entity.permalink}"
|
|
161
|
-
response = await call_put(client, url, json=entity.model_dump())
|
|
162
|
-
result = EntityResponse.model_validate(response.json())
|
|
163
|
-
|
|
164
|
-
# Format semantic summary based on status code
|
|
165
|
-
action = "Created" if response.status_code == 201 else "Updated"
|
|
166
|
-
summary = [
|
|
167
|
-
f"# {action} note",
|
|
168
|
-
f"project: {active_project.name}",
|
|
169
|
-
f"file_path: {result.file_path}",
|
|
170
|
-
f"permalink: {result.permalink}",
|
|
171
|
-
f"checksum: {result.checksum[:8] if result.checksum else 'unknown'}",
|
|
172
|
-
]
|
|
173
|
-
|
|
174
|
-
# Count observations by category
|
|
175
|
-
categories = {}
|
|
176
|
-
if result.observations:
|
|
177
|
-
for obs in result.observations:
|
|
178
|
-
categories[obs.category] = categories.get(obs.category, 0) + 1
|
|
179
|
-
|
|
180
|
-
summary.append("\n## Observations")
|
|
181
|
-
for category, count in sorted(categories.items()):
|
|
182
|
-
summary.append(f"- {category}: {count}")
|
|
183
|
-
|
|
184
|
-
# Count resolved/unresolved relations
|
|
185
|
-
unresolved = 0
|
|
186
|
-
resolved = 0
|
|
187
|
-
if result.relations:
|
|
188
|
-
unresolved = sum(1 for r in result.relations if not r.to_id)
|
|
189
|
-
resolved = len(result.relations) - unresolved
|
|
190
|
-
|
|
191
|
-
summary.append("\n## Relations")
|
|
192
|
-
summary.append(f"- Resolved: {resolved}")
|
|
193
|
-
if unresolved:
|
|
194
|
-
summary.append(f"- Unresolved: {unresolved}")
|
|
195
|
-
summary.append("\nNote: Unresolved relations point to entities that don't exist yet.")
|
|
196
|
-
summary.append(
|
|
197
|
-
"They will be automatically resolved when target entities are created or during sync operations."
|
|
125
|
+
|
|
126
|
+
# Get and validate the project (supports optional project parameter)
|
|
127
|
+
active_project = await get_active_project(client, project, context)
|
|
128
|
+
|
|
129
|
+
# Normalize "/" to empty string for root folder (must happen before validation)
|
|
130
|
+
if folder == "/":
|
|
131
|
+
folder = ""
|
|
132
|
+
|
|
133
|
+
# Validate folder path to prevent path traversal attacks
|
|
134
|
+
project_path = active_project.home
|
|
135
|
+
if folder and not validate_project_path(folder, project_path):
|
|
136
|
+
logger.warning(
|
|
137
|
+
"Attempted path traversal attack blocked",
|
|
138
|
+
folder=folder,
|
|
139
|
+
project=active_project.name,
|
|
198
140
|
)
|
|
141
|
+
return f"# Error\n\nFolder path '{folder}' is not allowed - paths must stay within project boundaries"
|
|
199
142
|
|
|
200
|
-
|
|
201
|
-
|
|
143
|
+
# Check migration status and wait briefly if needed
|
|
144
|
+
from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
|
|
202
145
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
146
|
+
migration_status = await wait_for_migration_or_return_status(
|
|
147
|
+
timeout=5.0, project_name=active_project.name
|
|
148
|
+
)
|
|
149
|
+
if migration_status: # pragma: no cover
|
|
150
|
+
return f"# System Status\n\n{migration_status}\n\nPlease wait for migration to complete before creating notes."
|
|
151
|
+
|
|
152
|
+
# Process tags using the helper function
|
|
153
|
+
tag_list = parse_tags(tags)
|
|
154
|
+
# Create the entity request
|
|
155
|
+
metadata = {"tags": tag_list} if tag_list else None
|
|
156
|
+
entity = Entity(
|
|
157
|
+
title=title,
|
|
158
|
+
folder=folder,
|
|
159
|
+
entity_type=entity_type,
|
|
160
|
+
content_type="text/markdown",
|
|
161
|
+
content=content,
|
|
162
|
+
entity_metadata=metadata,
|
|
163
|
+
)
|
|
164
|
+
project_url = active_project.permalink
|
|
165
|
+
|
|
166
|
+
# Create or update via knowledge API
|
|
167
|
+
logger.debug(f"Creating entity via API permalink={entity.permalink}")
|
|
168
|
+
url = f"{project_url}/knowledge/entities/{entity.permalink}"
|
|
169
|
+
response = await call_put(client, url, json=entity.model_dump())
|
|
170
|
+
result = EntityResponse.model_validate(response.json())
|
|
171
|
+
|
|
172
|
+
# Format semantic summary based on status code
|
|
173
|
+
action = "Created" if response.status_code == 201 else "Updated"
|
|
174
|
+
summary = [
|
|
175
|
+
f"# {action} note",
|
|
176
|
+
f"project: {active_project.name}",
|
|
177
|
+
f"file_path: {result.file_path}",
|
|
178
|
+
f"permalink: {result.permalink}",
|
|
179
|
+
f"checksum: {result.checksum[:8] if result.checksum else 'unknown'}",
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
# Count observations by category
|
|
183
|
+
categories = {}
|
|
184
|
+
if result.observations:
|
|
185
|
+
for obs in result.observations:
|
|
186
|
+
categories[obs.category] = categories.get(obs.category, 0) + 1
|
|
187
|
+
|
|
188
|
+
summary.append("\n## Observations")
|
|
189
|
+
for category, count in sorted(categories.items()):
|
|
190
|
+
summary.append(f"- {category}: {count}")
|
|
191
|
+
|
|
192
|
+
# Count resolved/unresolved relations
|
|
193
|
+
unresolved = 0
|
|
194
|
+
resolved = 0
|
|
195
|
+
if result.relations:
|
|
196
|
+
unresolved = sum(1 for r in result.relations if not r.to_id)
|
|
197
|
+
resolved = len(result.relations) - unresolved
|
|
198
|
+
|
|
199
|
+
summary.append("\n## Relations")
|
|
200
|
+
summary.append(f"- Resolved: {resolved}")
|
|
201
|
+
if unresolved:
|
|
202
|
+
summary.append(f"- Unresolved: {unresolved}")
|
|
203
|
+
summary.append(
|
|
204
|
+
"\nNote: Unresolved relations point to entities that don't exist yet."
|
|
205
|
+
)
|
|
206
|
+
summary.append(
|
|
207
|
+
"They will be automatically resolved when target entities are created or during sync operations."
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if tag_list:
|
|
211
|
+
summary.append(f"\n## Tags\n- {', '.join(tag_list)}")
|
|
212
|
+
|
|
213
|
+
# Log the response with structured data
|
|
214
|
+
logger.info(
|
|
215
|
+
f"MCP tool response: tool=write_note project={active_project.name} action={action} permalink={result.permalink} observations_count={len(result.observations)} relations_count={len(result.relations)} resolved_relations={resolved} unresolved_relations={unresolved} status_code={response.status_code}"
|
|
216
|
+
)
|
|
217
|
+
result = "\n".join(summary)
|
|
218
|
+
return add_project_metadata(result, active_project.name)
|