basic-memory 0.14.4__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 +5 -9
- 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/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 +139 -142
- 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 +76 -12
- basic_memory/db.py +104 -3
- basic_memory/deps.py +20 -3
- basic_memory/file_utils.py +37 -13
- basic_memory/ignore_utils.py +295 -0
- basic_memory/markdown/plugins.py +9 -7
- 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 +29 -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 +61 -228
- basic_memory/mcp/tools/read_content.py +28 -12
- basic_memory/mcp/tools/read_note.py +83 -46
- 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 +12 -6
- basic_memory/models/project.py +6 -2
- basic_memory/repository/entity_repository.py +29 -82
- basic_memory/repository/relation_repository.py +13 -0
- basic_memory/repository/repository.py +2 -2
- basic_memory/repository/search_repository.py +4 -2
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/base.py +39 -11
- basic_memory/schemas/cloud.py +46 -0
- basic_memory/schemas/memory.py +90 -21
- basic_memory/schemas/project_info.py +9 -10
- basic_memory/schemas/sync_report.py +48 -0
- basic_memory/services/context_service.py +25 -11
- basic_memory/services/entity_service.py +75 -45
- basic_memory/services/initialization.py +30 -11
- basic_memory/services/project_service.py +13 -23
- basic_memory/sync/sync_service.py +145 -21
- basic_memory/sync/watch_service.py +101 -40
- basic_memory/utils.py +14 -4
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.0.dist-info}/METADATA +7 -6
- basic_memory-0.15.0.dist-info/RECORD +147 -0
- basic_memory/mcp/project_session.py +0 -120
- basic_memory-0.14.4.dist-info/RECORD +0 -133
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.0.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.14.4.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,50 +19,76 @@ 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)
|
|
58
83
|
|
|
59
84
|
# Validate identifier to prevent path traversal attacks
|
|
60
85
|
# We need to check both the raw identifier and the processed path
|
|
61
86
|
processed_path = memory_url_path(identifier)
|
|
62
87
|
project_path = active_project.home
|
|
63
|
-
|
|
64
|
-
if not validate_project_path(identifier, project_path) or not validate_project_path(
|
|
88
|
+
|
|
89
|
+
if not validate_project_path(identifier, project_path) or not validate_project_path(
|
|
90
|
+
processed_path, project_path
|
|
91
|
+
):
|
|
65
92
|
logger.warning(
|
|
66
93
|
"Attempted path traversal attack blocked",
|
|
67
94
|
identifier=identifier,
|
|
@@ -83,7 +110,7 @@ async def read_note(
|
|
|
83
110
|
# Get the file via REST API - first try direct permalink lookup
|
|
84
111
|
entity_path = memory_url_path(identifier)
|
|
85
112
|
path = f"{project_url}/resource/{entity_path}"
|
|
86
|
-
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}")
|
|
87
114
|
|
|
88
115
|
try:
|
|
89
116
|
# Try direct lookup first
|
|
@@ -99,9 +126,12 @@ async def read_note(
|
|
|
99
126
|
|
|
100
127
|
# Fallback 1: Try title search via API
|
|
101
128
|
logger.info(f"Search title for: {identifier}")
|
|
102
|
-
title_results = await search_notes.fn(
|
|
129
|
+
title_results = await search_notes.fn(
|
|
130
|
+
query=identifier, search_type="title", project=project, context=context
|
|
131
|
+
)
|
|
103
132
|
|
|
104
|
-
|
|
133
|
+
# Handle both SearchResponse object and error strings
|
|
134
|
+
if title_results and hasattr(title_results, "results") and title_results.results:
|
|
105
135
|
result = title_results.results[0] # Get the first/best match
|
|
106
136
|
if result.permalink:
|
|
107
137
|
try:
|
|
@@ -119,58 +149,64 @@ async def read_note(
|
|
|
119
149
|
f"Failed to fetch content for found title match {result.permalink}: {e}"
|
|
120
150
|
)
|
|
121
151
|
else:
|
|
122
|
-
logger.info(
|
|
152
|
+
logger.info(
|
|
153
|
+
f"No results in title search for: {identifier} in project {active_project.name}"
|
|
154
|
+
)
|
|
123
155
|
|
|
124
156
|
# Fallback 2: Text search as a last resort
|
|
125
157
|
logger.info(f"Title search failed, trying text search for: {identifier}")
|
|
126
|
-
text_results = await search_notes.fn(
|
|
158
|
+
text_results = await search_notes.fn(
|
|
159
|
+
query=identifier, search_type="text", project=project, context=context
|
|
160
|
+
)
|
|
127
161
|
|
|
128
162
|
# We didn't find a direct match, construct a helpful error message
|
|
129
|
-
|
|
163
|
+
# Handle both SearchResponse object and error strings
|
|
164
|
+
if not text_results or not hasattr(text_results, "results") or not text_results.results:
|
|
130
165
|
# No results at all
|
|
131
|
-
return format_not_found_message(identifier)
|
|
166
|
+
return format_not_found_message(active_project.name, identifier)
|
|
132
167
|
else:
|
|
133
168
|
# We found some related results
|
|
134
|
-
return format_related_results(identifier, text_results.results[:5])
|
|
169
|
+
return format_related_results(active_project.name, identifier, text_results.results[:5])
|
|
135
170
|
|
|
136
171
|
|
|
137
|
-
def format_not_found_message(identifier: str) -> str:
|
|
172
|
+
def format_not_found_message(project: str | None, identifier: str) -> str:
|
|
138
173
|
"""Format a helpful message when no note was found."""
|
|
139
174
|
return dedent(f"""
|
|
140
|
-
# Note Not Found: "{identifier}"
|
|
141
|
-
|
|
175
|
+
# Note Not Found in {project}: "{identifier}"
|
|
176
|
+
|
|
142
177
|
I couldn't find any notes matching "{identifier}". Here are some suggestions:
|
|
143
|
-
|
|
178
|
+
|
|
144
179
|
## Check Identifier Type
|
|
145
180
|
- If you provided a title, try using the exact permalink instead
|
|
146
181
|
- If you provided a permalink, check for typos or try a broader search
|
|
147
|
-
|
|
182
|
+
|
|
148
183
|
## Search Instead
|
|
149
184
|
Try searching for related content:
|
|
150
185
|
```
|
|
151
|
-
search_notes(query="{identifier}")
|
|
186
|
+
search_notes(project="{project}", query="{identifier}")
|
|
152
187
|
```
|
|
153
|
-
|
|
188
|
+
|
|
154
189
|
## Recent Activity
|
|
155
190
|
Check recently modified notes:
|
|
156
191
|
```
|
|
157
192
|
recent_activity(timeframe="7d")
|
|
158
193
|
```
|
|
159
|
-
|
|
194
|
+
|
|
160
195
|
## Create New Note
|
|
161
196
|
This might be a good opportunity to create a new note on this topic:
|
|
162
197
|
```
|
|
163
198
|
write_note(
|
|
199
|
+
project="{project}",
|
|
164
200
|
title="{identifier.capitalize()}",
|
|
165
201
|
content='''
|
|
166
202
|
# {identifier.capitalize()}
|
|
167
|
-
|
|
203
|
+
|
|
168
204
|
## Overview
|
|
169
205
|
[Your content here]
|
|
170
|
-
|
|
206
|
+
|
|
171
207
|
## Observations
|
|
172
208
|
- [category] [Observation about {identifier}]
|
|
173
|
-
|
|
209
|
+
|
|
174
210
|
## Relations
|
|
175
211
|
- relates_to [[Related Topic]]
|
|
176
212
|
''',
|
|
@@ -180,13 +216,13 @@ def format_not_found_message(identifier: str) -> str:
|
|
|
180
216
|
""")
|
|
181
217
|
|
|
182
218
|
|
|
183
|
-
def format_related_results(identifier: str, results) -> str:
|
|
219
|
+
def format_related_results(project: str | None, identifier: str, results) -> str:
|
|
184
220
|
"""Format a helpful message with related results when an exact match wasn't found."""
|
|
185
221
|
message = dedent(f"""
|
|
186
|
-
# Note Not Found: "{identifier}"
|
|
187
|
-
|
|
222
|
+
# Note Not Found in {project}: "{identifier}"
|
|
223
|
+
|
|
188
224
|
I couldn't find an exact match for "{identifier}", but I found some related notes:
|
|
189
|
-
|
|
225
|
+
|
|
190
226
|
""")
|
|
191
227
|
|
|
192
228
|
for i, result in enumerate(results):
|
|
@@ -194,28 +230,29 @@ def format_related_results(identifier: str, results) -> str:
|
|
|
194
230
|
## {i + 1}. {result.title}
|
|
195
231
|
- **Type**: {result.type.value}
|
|
196
232
|
- **Permalink**: {result.permalink}
|
|
197
|
-
|
|
233
|
+
|
|
198
234
|
You can read this note with:
|
|
199
235
|
```
|
|
200
|
-
read_note("{result.permalink}")
|
|
236
|
+
read_note(project="{project}", {result.permalink}")
|
|
201
237
|
```
|
|
202
|
-
|
|
238
|
+
|
|
203
239
|
""")
|
|
204
240
|
|
|
205
|
-
message += dedent("""
|
|
241
|
+
message += dedent(f"""
|
|
206
242
|
## Try More Specific Lookup
|
|
207
243
|
For exact matches, try using the full permalink from one of the results above.
|
|
208
|
-
|
|
244
|
+
|
|
209
245
|
## Search For More Results
|
|
210
246
|
To see more related content:
|
|
211
247
|
```
|
|
212
|
-
search_notes(query="{identifier}")
|
|
248
|
+
search_notes(project="{project}", query="{identifier}")
|
|
213
249
|
```
|
|
214
|
-
|
|
250
|
+
|
|
215
251
|
## Create New Note
|
|
216
252
|
If none of these match what you're looking for, consider creating a new note:
|
|
217
253
|
```
|
|
218
254
|
write_note(
|
|
255
|
+
project="{project}",
|
|
219
256
|
title="[Your title]",
|
|
220
257
|
content="[Your content]",
|
|
221
258
|
folder="notes"
|