basic-memory 0.14.4__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/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +5 -9
- basic_memory/api/app.py +10 -4
- basic_memory/api/routers/directory_router.py +23 -2
- basic_memory/api/routers/knowledge_router.py +25 -8
- basic_memory/api/routers/project_router.py +100 -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 +43 -0
- basic_memory/cli/commands/import_memory_json.py +0 -4
- basic_memory/cli/commands/mcp.py +77 -60
- basic_memory/cli/commands/project.py +154 -152
- basic_memory/cli/commands/status.py +25 -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 +131 -21
- basic_memory/db.py +104 -3
- basic_memory/deps.py +27 -8
- 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 +124 -14
- 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 +17 -16
- basic_memory/mcp/prompts/recent_activity.py +116 -32
- basic_memory/mcp/prompts/search.py +13 -12
- basic_memory/mcp/prompts/utils.py +11 -4
- basic_memory/mcp/resources/ai_assistant_guide.md +211 -341
- basic_memory/mcp/resources/project_info.py +27 -11
- basic_memory/mcp/server.py +0 -37
- basic_memory/mcp/tools/__init__.py +5 -6
- basic_memory/mcp/tools/build_context.py +67 -56
- basic_memory/mcp/tools/canvas.py +38 -26
- basic_memory/mcp/tools/chatgpt_tools.py +187 -0
- basic_memory/mcp/tools/delete_note.py +81 -47
- basic_memory/mcp/tools/edit_note.py +155 -138
- basic_memory/mcp/tools/list_directory.py +112 -99
- basic_memory/mcp/tools/move_note.py +181 -101
- basic_memory/mcp/tools/project_management.py +113 -277
- basic_memory/mcp/tools/read_content.py +91 -74
- basic_memory/mcp/tools/read_note.py +152 -115
- basic_memory/mcp/tools/recent_activity.py +471 -68
- basic_memory/mcp/tools/search.py +105 -92
- basic_memory/mcp/tools/sync_status.py +136 -130
- basic_memory/mcp/tools/utils.py +4 -0
- basic_memory/mcp/tools/view_note.py +44 -33
- basic_memory/mcp/tools/write_note.py +151 -90
- basic_memory/models/knowledge.py +12 -6
- basic_memory/models/project.py +6 -2
- basic_memory/repository/entity_repository.py +89 -82
- basic_memory/repository/relation_repository.py +13 -0
- basic_memory/repository/repository.py +18 -5
- basic_memory/repository/search_repository.py +46 -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/directory_service.py +124 -3
- basic_memory/services/entity_service.py +100 -48
- basic_memory/services/initialization.py +30 -11
- basic_memory/services/project_service.py +101 -24
- basic_memory/services/search_service.py +16 -8
- basic_memory/sync/sync_service.py +173 -34
- 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.1.dist-info}/METADATA +57 -9
- basic_memory-0.15.1.dist-info/RECORD +146 -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.1.dist-info}/WHEEL +0 -0
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.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
|
-
from basic_memory.mcp.async_client import
|
|
9
|
+
from basic_memory.mcp.async_client import get_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,159 +19,194 @@ 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
|
"""
|
|
80
|
+
async with get_client() as client:
|
|
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"
|
|
99
|
+
|
|
100
|
+
# Check migration status and wait briefly if needed
|
|
101
|
+
from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
|
|
102
|
+
|
|
103
|
+
migration_status = await wait_for_migration_or_return_status(
|
|
104
|
+
timeout=5.0, project_name=active_project.name
|
|
105
|
+
)
|
|
106
|
+
if migration_status: # pragma: no cover
|
|
107
|
+
return f"# System Status\n\n{migration_status}\n\nPlease wait for migration to complete before reading notes."
|
|
108
|
+
project_url = active_project.project_url
|
|
109
|
+
|
|
110
|
+
# Get the file via REST API - first try direct permalink lookup
|
|
111
|
+
entity_path = memory_url_path(identifier)
|
|
112
|
+
path = f"{project_url}/resource/{entity_path}"
|
|
113
|
+
logger.info(f"Attempting to read note from Project: {active_project.name} URL: {path}")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
# Try direct lookup first
|
|
117
|
+
response = await call_get(client, path, params={"page": page, "page_size": page_size})
|
|
118
|
+
|
|
119
|
+
# If successful, return the content
|
|
120
|
+
if response.status_code == 200:
|
|
121
|
+
logger.info("Returning read_note result from resource: {path}", path=entity_path)
|
|
122
|
+
return response.text
|
|
123
|
+
except Exception as e: # pragma: no cover
|
|
124
|
+
logger.info(f"Direct lookup failed for '{path}': {e}")
|
|
125
|
+
# Continue to fallback methods
|
|
126
|
+
|
|
127
|
+
# Fallback 1: Try title search via API
|
|
128
|
+
logger.info(f"Search title for: {identifier}")
|
|
129
|
+
title_results = await search_notes.fn(
|
|
130
|
+
query=identifier, search_type="title", project=project, context=context
|
|
131
|
+
)
|
|
55
132
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
133
|
+
# Handle both SearchResponse object and error strings
|
|
134
|
+
if title_results and hasattr(title_results, "results") and title_results.results:
|
|
135
|
+
result = title_results.results[0] # Get the first/best match
|
|
136
|
+
if result.permalink:
|
|
137
|
+
try:
|
|
138
|
+
# Try to fetch the content using the found permalink
|
|
139
|
+
path = f"{project_url}/resource/{result.permalink}"
|
|
140
|
+
response = await call_get(
|
|
141
|
+
client, path, params={"page": page, "page_size": page_size}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if response.status_code == 200:
|
|
145
|
+
logger.info(f"Found note by title search: {result.permalink}")
|
|
146
|
+
return response.text
|
|
147
|
+
except Exception as e: # pragma: no cover
|
|
148
|
+
logger.info(
|
|
149
|
+
f"Failed to fetch content for found title match {result.permalink}: {e}"
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
logger.info(
|
|
153
|
+
f"No results in title search for: {identifier} in project {active_project.name}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Fallback 2: Text search as a last resort
|
|
157
|
+
logger.info(f"Title search failed, trying text search for: {identifier}")
|
|
158
|
+
text_results = await search_notes.fn(
|
|
159
|
+
query=identifier, search_type="text", project=project, context=context
|
|
70
160
|
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# Get the file via REST API - first try direct permalink lookup
|
|
84
|
-
entity_path = memory_url_path(identifier)
|
|
85
|
-
path = f"{project_url}/resource/{entity_path}"
|
|
86
|
-
logger.info(f"Attempting to read note from URL: {path}")
|
|
87
|
-
|
|
88
|
-
try:
|
|
89
|
-
# Try direct lookup first
|
|
90
|
-
response = await call_get(client, path, params={"page": page, "page_size": page_size})
|
|
91
|
-
|
|
92
|
-
# If successful, return the content
|
|
93
|
-
if response.status_code == 200:
|
|
94
|
-
logger.info("Returning read_note result from resource: {path}", path=entity_path)
|
|
95
|
-
return response.text
|
|
96
|
-
except Exception as e: # pragma: no cover
|
|
97
|
-
logger.info(f"Direct lookup failed for '{path}': {e}")
|
|
98
|
-
# Continue to fallback methods
|
|
99
|
-
|
|
100
|
-
# Fallback 1: Try title search via API
|
|
101
|
-
logger.info(f"Search title for: {identifier}")
|
|
102
|
-
title_results = await search_notes.fn(query=identifier, search_type="title", project=project)
|
|
103
|
-
|
|
104
|
-
if title_results and title_results.results:
|
|
105
|
-
result = title_results.results[0] # Get the first/best match
|
|
106
|
-
if result.permalink:
|
|
107
|
-
try:
|
|
108
|
-
# Try to fetch the content using the found permalink
|
|
109
|
-
path = f"{project_url}/resource/{result.permalink}"
|
|
110
|
-
response = await call_get(
|
|
111
|
-
client, path, params={"page": page, "page_size": page_size}
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
if response.status_code == 200:
|
|
115
|
-
logger.info(f"Found note by title search: {result.permalink}")
|
|
116
|
-
return response.text
|
|
117
|
-
except Exception as e: # pragma: no cover
|
|
118
|
-
logger.info(
|
|
119
|
-
f"Failed to fetch content for found title match {result.permalink}: {e}"
|
|
120
|
-
)
|
|
121
|
-
else:
|
|
122
|
-
logger.info(f"No results in title search for: {identifier}")
|
|
123
|
-
|
|
124
|
-
# Fallback 2: Text search as a last resort
|
|
125
|
-
logger.info(f"Title search failed, trying text search for: {identifier}")
|
|
126
|
-
text_results = await search_notes.fn(query=identifier, search_type="text", project=project)
|
|
127
|
-
|
|
128
|
-
# We didn't find a direct match, construct a helpful error message
|
|
129
|
-
if not text_results or not text_results.results:
|
|
130
|
-
# No results at all
|
|
131
|
-
return format_not_found_message(identifier)
|
|
132
|
-
else:
|
|
133
|
-
# We found some related results
|
|
134
|
-
return format_related_results(identifier, text_results.results[:5])
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def format_not_found_message(identifier: str) -> str:
|
|
161
|
+
|
|
162
|
+
# We didn't find a direct match, construct a helpful error message
|
|
163
|
+
# Handle both SearchResponse object and error strings
|
|
164
|
+
if not text_results or not hasattr(text_results, "results") or not text_results.results:
|
|
165
|
+
# No results at all
|
|
166
|
+
return format_not_found_message(active_project.name, identifier)
|
|
167
|
+
else:
|
|
168
|
+
# We found some related results
|
|
169
|
+
return format_related_results(active_project.name, identifier, text_results.results[:5])
|
|
170
|
+
|
|
171
|
+
|
|
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"
|