basic-memory 0.14.3__py3-none-any.whl → 0.15.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of basic-memory might be problematic. Click here for more details.
- basic_memory/__init__.py +1 -1
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
- basic_memory/api/app.py +10 -4
- basic_memory/api/routers/knowledge_router.py +25 -8
- basic_memory/api/routers/project_router.py +99 -4
- basic_memory/api/routers/resource_router.py +3 -3
- basic_memory/cli/app.py +9 -28
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/cloud/__init__.py +5 -0
- basic_memory/cli/commands/cloud/api_client.py +112 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +818 -0
- basic_memory/cli/commands/cloud/core_commands.py +288 -0
- basic_memory/cli/commands/cloud/mount_commands.py +295 -0
- basic_memory/cli/commands/cloud/rclone_config.py +288 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +198 -0
- basic_memory/cli/commands/command_utils.py +60 -0
- basic_memory/cli/commands/import_memory_json.py +0 -4
- basic_memory/cli/commands/mcp.py +16 -4
- basic_memory/cli/commands/project.py +141 -145
- basic_memory/cli/commands/status.py +34 -22
- basic_memory/cli/commands/sync.py +45 -228
- basic_memory/cli/commands/tool.py +87 -16
- basic_memory/cli/main.py +1 -0
- basic_memory/config.py +96 -20
- basic_memory/db.py +104 -3
- basic_memory/deps.py +20 -3
- basic_memory/file_utils.py +89 -0
- basic_memory/ignore_utils.py +295 -0
- basic_memory/importers/chatgpt_importer.py +1 -1
- basic_memory/importers/utils.py +2 -2
- basic_memory/markdown/entity_parser.py +2 -2
- basic_memory/markdown/markdown_processor.py +2 -2
- basic_memory/markdown/plugins.py +39 -21
- basic_memory/markdown/utils.py +1 -1
- basic_memory/mcp/async_client.py +22 -10
- basic_memory/mcp/project_context.py +141 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +49 -4
- basic_memory/mcp/prompts/continue_conversation.py +1 -1
- basic_memory/mcp/prompts/recent_activity.py +116 -32
- basic_memory/mcp/prompts/search.py +1 -1
- basic_memory/mcp/prompts/utils.py +11 -4
- basic_memory/mcp/resources/ai_assistant_guide.md +179 -41
- basic_memory/mcp/resources/project_info.py +20 -6
- basic_memory/mcp/server.py +0 -37
- basic_memory/mcp/tools/__init__.py +5 -6
- basic_memory/mcp/tools/build_context.py +39 -19
- basic_memory/mcp/tools/canvas.py +19 -8
- basic_memory/mcp/tools/chatgpt_tools.py +178 -0
- basic_memory/mcp/tools/delete_note.py +67 -34
- basic_memory/mcp/tools/edit_note.py +55 -39
- basic_memory/mcp/tools/headers.py +44 -0
- basic_memory/mcp/tools/list_directory.py +18 -8
- basic_memory/mcp/tools/move_note.py +119 -41
- basic_memory/mcp/tools/project_management.py +77 -229
- basic_memory/mcp/tools/read_content.py +28 -12
- basic_memory/mcp/tools/read_note.py +97 -57
- basic_memory/mcp/tools/recent_activity.py +441 -42
- basic_memory/mcp/tools/search.py +82 -70
- basic_memory/mcp/tools/sync_status.py +5 -4
- basic_memory/mcp/tools/utils.py +19 -0
- basic_memory/mcp/tools/view_note.py +31 -6
- basic_memory/mcp/tools/write_note.py +65 -14
- basic_memory/models/knowledge.py +19 -2
- basic_memory/models/project.py +6 -2
- basic_memory/repository/entity_repository.py +31 -84
- basic_memory/repository/project_repository.py +1 -1
- basic_memory/repository/relation_repository.py +13 -0
- basic_memory/repository/repository.py +2 -2
- basic_memory/repository/search_repository.py +9 -3
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/base.py +70 -12
- basic_memory/schemas/cloud.py +46 -0
- basic_memory/schemas/memory.py +99 -18
- basic_memory/schemas/project_info.py +9 -10
- basic_memory/schemas/sync_report.py +48 -0
- basic_memory/services/context_service.py +35 -11
- basic_memory/services/directory_service.py +7 -0
- basic_memory/services/entity_service.py +82 -52
- basic_memory/services/initialization.py +30 -11
- basic_memory/services/project_service.py +23 -33
- basic_memory/sync/sync_service.py +148 -24
- basic_memory/sync/watch_service.py +128 -44
- basic_memory/utils.py +181 -109
- {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/METADATA +26 -96
- basic_memory-0.15.0.dist-info/RECORD +147 -0
- basic_memory/mcp/project_session.py +0 -120
- basic_memory-0.14.3.dist-info/RECORD +0 -132
- {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,12 +4,13 @@ from textwrap import dedent
|
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
6
|
from loguru import logger
|
|
7
|
+
from fastmcp import Context
|
|
7
8
|
|
|
8
9
|
from basic_memory.mcp.async_client import client
|
|
10
|
+
from basic_memory.mcp.project_context import get_active_project
|
|
9
11
|
from basic_memory.mcp.server import mcp
|
|
10
12
|
from basic_memory.mcp.tools.search import search_notes
|
|
11
13
|
from basic_memory.mcp.tools.utils import call_get
|
|
12
|
-
from basic_memory.mcp.project_session import get_active_project
|
|
13
14
|
from basic_memory.schemas.memory import memory_url_path
|
|
14
15
|
from basic_memory.utils import validate_project_path
|
|
15
16
|
|
|
@@ -18,43 +19,83 @@ from basic_memory.utils import validate_project_path
|
|
|
18
19
|
description="Read a markdown note by title or permalink.",
|
|
19
20
|
)
|
|
20
21
|
async def read_note(
|
|
21
|
-
identifier: str,
|
|
22
|
+
identifier: str,
|
|
23
|
+
project: Optional[str] = None,
|
|
24
|
+
page: int = 1,
|
|
25
|
+
page_size: int = 10,
|
|
26
|
+
context: Context | None = None,
|
|
22
27
|
) -> str:
|
|
23
|
-
"""
|
|
28
|
+
"""Return the raw markdown for a note, or guidance text if no match is found.
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
Finds and retrieves a note by its title, permalink, or content search,
|
|
26
31
|
returning the raw markdown content including observations, relations, and metadata.
|
|
27
|
-
|
|
32
|
+
|
|
33
|
+
Project Resolution:
|
|
34
|
+
Server resolves projects in this order: Single Project Mode → project parameter → default project.
|
|
35
|
+
If project unknown, use list_memory_projects() or recent_activity() first.
|
|
36
|
+
|
|
37
|
+
This tool will try multiple lookup strategies to find the most relevant note:
|
|
38
|
+
1. Direct permalink lookup
|
|
39
|
+
2. Title search fallback
|
|
40
|
+
3. Text search as last resort
|
|
28
41
|
|
|
29
42
|
Args:
|
|
43
|
+
project: Project name to read from. Optional - server will resolve using the
|
|
44
|
+
hierarchy above. If unknown, use list_memory_projects() to discover
|
|
45
|
+
available projects.
|
|
30
46
|
identifier: The title or permalink of the note to read
|
|
31
47
|
Can be a full memory:// URL, a permalink, a title, or search text
|
|
32
48
|
page: Page number for paginated results (default: 1)
|
|
33
49
|
page_size: Number of items per page (default: 10)
|
|
34
|
-
|
|
50
|
+
context: Optional FastMCP context for performance caching.
|
|
35
51
|
|
|
36
52
|
Returns:
|
|
37
53
|
The full markdown content of the note if found, or helpful guidance if not found.
|
|
54
|
+
Content includes frontmatter, observations, relations, and all markdown formatting.
|
|
38
55
|
|
|
39
56
|
Examples:
|
|
40
57
|
# Read by permalink
|
|
41
|
-
read_note("specs/search-spec")
|
|
58
|
+
read_note("my-research", "specs/search-spec")
|
|
42
59
|
|
|
43
60
|
# Read by title
|
|
44
|
-
read_note("Search Specification")
|
|
61
|
+
read_note("work-project", "Search Specification")
|
|
45
62
|
|
|
46
63
|
# Read with memory URL
|
|
47
|
-
read_note("memory://specs/search-spec")
|
|
64
|
+
read_note("my-research", "memory://specs/search-spec")
|
|
48
65
|
|
|
49
66
|
# Read with pagination
|
|
50
|
-
read_note("Project Updates", page=2, page_size=5)
|
|
67
|
+
read_note("work-project", "Project Updates", page=2, page_size=5)
|
|
68
|
+
|
|
69
|
+
# Read recent meeting notes
|
|
70
|
+
read_note("team-docs", "Weekly Standup")
|
|
51
71
|
|
|
52
|
-
|
|
53
|
-
|
|
72
|
+
Raises:
|
|
73
|
+
HTTPError: If project doesn't exist or is inaccessible
|
|
74
|
+
SecurityError: If identifier attempts path traversal
|
|
75
|
+
|
|
76
|
+
Note:
|
|
77
|
+
If the exact note isn't found, this tool provides helpful suggestions
|
|
78
|
+
including related notes, search commands, and note creation templates.
|
|
54
79
|
"""
|
|
55
80
|
|
|
56
|
-
# Get
|
|
57
|
-
active_project = get_active_project(project)
|
|
81
|
+
# Get and validate the project
|
|
82
|
+
active_project = await get_active_project(client, project, context)
|
|
83
|
+
|
|
84
|
+
# Validate identifier to prevent path traversal attacks
|
|
85
|
+
# We need to check both the raw identifier and the processed path
|
|
86
|
+
processed_path = memory_url_path(identifier)
|
|
87
|
+
project_path = active_project.home
|
|
88
|
+
|
|
89
|
+
if not validate_project_path(identifier, project_path) or not validate_project_path(
|
|
90
|
+
processed_path, project_path
|
|
91
|
+
):
|
|
92
|
+
logger.warning(
|
|
93
|
+
"Attempted path traversal attack blocked",
|
|
94
|
+
identifier=identifier,
|
|
95
|
+
processed_path=processed_path,
|
|
96
|
+
project=active_project.name,
|
|
97
|
+
)
|
|
98
|
+
return f"# Error\n\nIdentifier '{identifier}' is not allowed - paths must stay within project boundaries"
|
|
58
99
|
|
|
59
100
|
# Check migration status and wait briefly if needed
|
|
60
101
|
from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
|
|
@@ -68,19 +109,8 @@ async def read_note(
|
|
|
68
109
|
|
|
69
110
|
# Get the file via REST API - first try direct permalink lookup
|
|
70
111
|
entity_path = memory_url_path(identifier)
|
|
71
|
-
|
|
72
|
-
# Validate path to prevent path traversal attacks
|
|
73
|
-
project_path = active_project.home
|
|
74
|
-
if not validate_project_path(entity_path, project_path):
|
|
75
|
-
logger.warning(
|
|
76
|
-
"Attempted path traversal attack blocked",
|
|
77
|
-
identifier=identifier,
|
|
78
|
-
entity_path=entity_path,
|
|
79
|
-
project=active_project.name,
|
|
80
|
-
)
|
|
81
|
-
return f"# Error\n\nPath '{identifier}' is not allowed - paths must stay within project boundaries"
|
|
82
112
|
path = f"{project_url}/resource/{entity_path}"
|
|
83
|
-
logger.info(f"Attempting to read note from URL: {path}")
|
|
113
|
+
logger.info(f"Attempting to read note from Project: {active_project.name} URL: {path}")
|
|
84
114
|
|
|
85
115
|
try:
|
|
86
116
|
# Try direct lookup first
|
|
@@ -96,9 +126,12 @@ async def read_note(
|
|
|
96
126
|
|
|
97
127
|
# Fallback 1: Try title search via API
|
|
98
128
|
logger.info(f"Search title for: {identifier}")
|
|
99
|
-
title_results = await search_notes.fn(
|
|
129
|
+
title_results = await search_notes.fn(
|
|
130
|
+
query=identifier, search_type="title", project=project, context=context
|
|
131
|
+
)
|
|
100
132
|
|
|
101
|
-
|
|
133
|
+
# Handle both SearchResponse object and error strings
|
|
134
|
+
if title_results and hasattr(title_results, "results") and title_results.results:
|
|
102
135
|
result = title_results.results[0] # Get the first/best match
|
|
103
136
|
if result.permalink:
|
|
104
137
|
try:
|
|
@@ -116,58 +149,64 @@ async def read_note(
|
|
|
116
149
|
f"Failed to fetch content for found title match {result.permalink}: {e}"
|
|
117
150
|
)
|
|
118
151
|
else:
|
|
119
|
-
logger.info(
|
|
152
|
+
logger.info(
|
|
153
|
+
f"No results in title search for: {identifier} in project {active_project.name}"
|
|
154
|
+
)
|
|
120
155
|
|
|
121
156
|
# Fallback 2: Text search as a last resort
|
|
122
157
|
logger.info(f"Title search failed, trying text search for: {identifier}")
|
|
123
|
-
text_results = await search_notes.fn(
|
|
158
|
+
text_results = await search_notes.fn(
|
|
159
|
+
query=identifier, search_type="text", project=project, context=context
|
|
160
|
+
)
|
|
124
161
|
|
|
125
162
|
# We didn't find a direct match, construct a helpful error message
|
|
126
|
-
|
|
163
|
+
# Handle both SearchResponse object and error strings
|
|
164
|
+
if not text_results or not hasattr(text_results, "results") or not text_results.results:
|
|
127
165
|
# No results at all
|
|
128
|
-
return format_not_found_message(identifier)
|
|
166
|
+
return format_not_found_message(active_project.name, identifier)
|
|
129
167
|
else:
|
|
130
168
|
# We found some related results
|
|
131
|
-
return format_related_results(identifier, text_results.results[:5])
|
|
169
|
+
return format_related_results(active_project.name, identifier, text_results.results[:5])
|
|
132
170
|
|
|
133
171
|
|
|
134
|
-
def format_not_found_message(identifier: str) -> str:
|
|
172
|
+
def format_not_found_message(project: str | None, identifier: str) -> str:
|
|
135
173
|
"""Format a helpful message when no note was found."""
|
|
136
174
|
return dedent(f"""
|
|
137
|
-
# Note Not Found: "{identifier}"
|
|
138
|
-
|
|
139
|
-
I
|
|
140
|
-
|
|
175
|
+
# Note Not Found in {project}: "{identifier}"
|
|
176
|
+
|
|
177
|
+
I couldn't find any notes matching "{identifier}". Here are some suggestions:
|
|
178
|
+
|
|
141
179
|
## Check Identifier Type
|
|
142
180
|
- If you provided a title, try using the exact permalink instead
|
|
143
181
|
- If you provided a permalink, check for typos or try a broader search
|
|
144
|
-
|
|
182
|
+
|
|
145
183
|
## Search Instead
|
|
146
184
|
Try searching for related content:
|
|
147
185
|
```
|
|
148
|
-
search_notes(query="{identifier}")
|
|
186
|
+
search_notes(project="{project}", query="{identifier}")
|
|
149
187
|
```
|
|
150
|
-
|
|
188
|
+
|
|
151
189
|
## Recent Activity
|
|
152
190
|
Check recently modified notes:
|
|
153
191
|
```
|
|
154
192
|
recent_activity(timeframe="7d")
|
|
155
193
|
```
|
|
156
|
-
|
|
194
|
+
|
|
157
195
|
## Create New Note
|
|
158
196
|
This might be a good opportunity to create a new note on this topic:
|
|
159
197
|
```
|
|
160
198
|
write_note(
|
|
199
|
+
project="{project}",
|
|
161
200
|
title="{identifier.capitalize()}",
|
|
162
201
|
content='''
|
|
163
202
|
# {identifier.capitalize()}
|
|
164
|
-
|
|
203
|
+
|
|
165
204
|
## Overview
|
|
166
205
|
[Your content here]
|
|
167
|
-
|
|
206
|
+
|
|
168
207
|
## Observations
|
|
169
208
|
- [category] [Observation about {identifier}]
|
|
170
|
-
|
|
209
|
+
|
|
171
210
|
## Relations
|
|
172
211
|
- relates_to [[Related Topic]]
|
|
173
212
|
''',
|
|
@@ -177,13 +216,13 @@ def format_not_found_message(identifier: str) -> str:
|
|
|
177
216
|
""")
|
|
178
217
|
|
|
179
218
|
|
|
180
|
-
def format_related_results(identifier: str, results) -> str:
|
|
219
|
+
def format_related_results(project: str | None, identifier: str, results) -> str:
|
|
181
220
|
"""Format a helpful message with related results when an exact match wasn't found."""
|
|
182
221
|
message = dedent(f"""
|
|
183
|
-
# Note Not Found: "{identifier}"
|
|
184
|
-
|
|
185
|
-
I
|
|
186
|
-
|
|
222
|
+
# Note Not Found in {project}: "{identifier}"
|
|
223
|
+
|
|
224
|
+
I couldn't find an exact match for "{identifier}", but I found some related notes:
|
|
225
|
+
|
|
187
226
|
""")
|
|
188
227
|
|
|
189
228
|
for i, result in enumerate(results):
|
|
@@ -191,28 +230,29 @@ def format_related_results(identifier: str, results) -> str:
|
|
|
191
230
|
## {i + 1}. {result.title}
|
|
192
231
|
- **Type**: {result.type.value}
|
|
193
232
|
- **Permalink**: {result.permalink}
|
|
194
|
-
|
|
233
|
+
|
|
195
234
|
You can read this note with:
|
|
196
235
|
```
|
|
197
|
-
read_note("{result.permalink}")
|
|
236
|
+
read_note(project="{project}", {result.permalink}")
|
|
198
237
|
```
|
|
199
|
-
|
|
238
|
+
|
|
200
239
|
""")
|
|
201
240
|
|
|
202
|
-
message += dedent("""
|
|
241
|
+
message += dedent(f"""
|
|
203
242
|
## Try More Specific Lookup
|
|
204
243
|
For exact matches, try using the full permalink from one of the results above.
|
|
205
|
-
|
|
244
|
+
|
|
206
245
|
## Search For More Results
|
|
207
246
|
To see more related content:
|
|
208
247
|
```
|
|
209
|
-
search_notes(query="{identifier}")
|
|
248
|
+
search_notes(project="{project}", query="{identifier}")
|
|
210
249
|
```
|
|
211
|
-
|
|
250
|
+
|
|
212
251
|
## Create New Note
|
|
213
252
|
If none of these match what you're looking for, consider creating a new note:
|
|
214
253
|
```
|
|
215
254
|
write_note(
|
|
255
|
+
project="{project}",
|
|
216
256
|
title="[Your title]",
|
|
217
257
|
content="[Your content]",
|
|
218
258
|
folder="notes"
|