basic-memory 0.14.2__py3-none-any.whl → 0.14.4__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/env.py +3 -1
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +53 -0
- basic_memory/api/app.py +4 -1
- basic_memory/api/routers/management_router.py +3 -1
- basic_memory/api/routers/project_router.py +21 -13
- basic_memory/api/routers/resource_router.py +3 -3
- basic_memory/cli/app.py +3 -3
- basic_memory/cli/commands/__init__.py +1 -2
- basic_memory/cli/commands/db.py +5 -5
- basic_memory/cli/commands/import_chatgpt.py +3 -2
- basic_memory/cli/commands/import_claude_conversations.py +3 -1
- basic_memory/cli/commands/import_claude_projects.py +3 -1
- basic_memory/cli/commands/import_memory_json.py +5 -2
- basic_memory/cli/commands/mcp.py +3 -15
- basic_memory/cli/commands/project.py +46 -6
- basic_memory/cli/commands/status.py +4 -1
- basic_memory/cli/commands/sync.py +10 -2
- basic_memory/cli/main.py +0 -1
- basic_memory/config.py +61 -34
- basic_memory/db.py +2 -6
- basic_memory/deps.py +3 -2
- basic_memory/file_utils.py +65 -0
- basic_memory/importers/chatgpt_importer.py +20 -10
- basic_memory/importers/memory_json_importer.py +22 -7
- 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 +42 -26
- basic_memory/markdown/utils.py +1 -1
- basic_memory/mcp/async_client.py +22 -2
- basic_memory/mcp/project_session.py +6 -4
- basic_memory/mcp/prompts/__init__.py +0 -2
- basic_memory/mcp/server.py +8 -71
- basic_memory/mcp/tools/build_context.py +12 -2
- basic_memory/mcp/tools/move_note.py +24 -12
- basic_memory/mcp/tools/project_management.py +22 -7
- basic_memory/mcp/tools/read_content.py +16 -0
- basic_memory/mcp/tools/read_note.py +17 -2
- basic_memory/mcp/tools/sync_status.py +3 -2
- basic_memory/mcp/tools/write_note.py +9 -1
- basic_memory/models/knowledge.py +13 -2
- basic_memory/models/project.py +3 -3
- basic_memory/repository/entity_repository.py +2 -2
- basic_memory/repository/project_repository.py +19 -1
- basic_memory/repository/search_repository.py +7 -3
- basic_memory/schemas/base.py +40 -10
- basic_memory/schemas/importer.py +1 -0
- basic_memory/schemas/memory.py +23 -11
- basic_memory/services/context_service.py +12 -2
- basic_memory/services/directory_service.py +7 -0
- basic_memory/services/entity_service.py +56 -10
- basic_memory/services/initialization.py +0 -75
- basic_memory/services/project_service.py +93 -36
- basic_memory/sync/background_sync.py +4 -3
- basic_memory/sync/sync_service.py +53 -4
- basic_memory/sync/watch_service.py +31 -8
- basic_memory/utils.py +234 -71
- {basic_memory-0.14.2.dist-info → basic_memory-0.14.4.dist-info}/METADATA +21 -92
- {basic_memory-0.14.2.dist-info → basic_memory-0.14.4.dist-info}/RECORD +63 -68
- basic_memory/cli/commands/auth.py +0 -136
- basic_memory/mcp/auth_provider.py +0 -270
- basic_memory/mcp/external_auth_provider.py +0 -321
- basic_memory/mcp/prompts/sync_status.py +0 -112
- basic_memory/mcp/supabase_auth_provider.py +0 -463
- basic_memory/services/migration_service.py +0 -168
- {basic_memory-0.14.2.dist-info → basic_memory-0.14.4.dist-info}/WHEEL +0 -0
- {basic_memory-0.14.2.dist-info → basic_memory-0.14.4.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.14.2.dist-info → basic_memory-0.14.4.dist-info}/licenses/LICENSE +0 -0
basic_memory/utils.py
CHANGED
|
@@ -5,11 +5,12 @@ import os
|
|
|
5
5
|
import logging
|
|
6
6
|
import re
|
|
7
7
|
import sys
|
|
8
|
-
import
|
|
8
|
+
from datetime import datetime
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Optional, Protocol, Union, runtime_checkable, List
|
|
10
|
+
from typing import Optional, Protocol, Union, runtime_checkable, List
|
|
11
11
|
|
|
12
12
|
from loguru import logger
|
|
13
|
+
from unidecode import unidecode
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
@runtime_checkable
|
|
@@ -27,9 +28,11 @@ FilePath = Union[Path, str]
|
|
|
27
28
|
logging.getLogger("opentelemetry.sdk.metrics._internal.instrument").setLevel(logging.ERROR)
|
|
28
29
|
|
|
29
30
|
|
|
30
|
-
def generate_permalink(file_path: Union[Path, str,
|
|
31
|
-
"""
|
|
32
|
-
|
|
31
|
+
def generate_permalink(file_path: Union[Path, str, PathLike], split_extension: bool = True) -> str:
|
|
32
|
+
"""Generate a stable permalink from a file path.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
file_path: Original file path (str, Path, or PathLike)
|
|
33
36
|
|
|
34
37
|
Returns:
|
|
35
38
|
Normalized permalink that matches validation rules. Converts spaces and underscores
|
|
@@ -38,7 +41,7 @@ def generate_permalink(file_path: Union[Path, str, Any]) -> str:
|
|
|
38
41
|
Examples:
|
|
39
42
|
>>> generate_permalink("docs/My Feature.md")
|
|
40
43
|
'docs/my-feature'
|
|
41
|
-
>>> generate_permalink("specs/
|
|
44
|
+
>>> generate_permalink("specs/API (v2).md")
|
|
42
45
|
'specs/api-v2'
|
|
43
46
|
>>> generate_permalink("design/unified_model_refactor.md")
|
|
44
47
|
'design/unified-model-refactor'
|
|
@@ -46,84 +49,99 @@ def generate_permalink(file_path: Union[Path, str, Any]) -> str:
|
|
|
46
49
|
'中文/测试文档'
|
|
47
50
|
"""
|
|
48
51
|
# Convert Path to string if needed
|
|
49
|
-
path_str = str(file_path)
|
|
50
|
-
|
|
51
|
-
# Remove extension
|
|
52
|
-
base = os.path.splitext(path_str)
|
|
53
|
-
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"ñ": "n", # Handle Niño -> nino
|
|
65
|
-
"ö": "o", # Handle Björk -> bjork
|
|
66
|
-
"ä": "a", # Handle Häagen -> haagen
|
|
67
|
-
# Add more mappings as needed
|
|
68
|
-
}
|
|
52
|
+
path_str = Path(str(file_path)).as_posix()
|
|
53
|
+
|
|
54
|
+
# Remove extension (for now, possibly)
|
|
55
|
+
(base, extension) = os.path.splitext(path_str)
|
|
56
|
+
|
|
57
|
+
# Check if we have CJK characters that should be preserved
|
|
58
|
+
# CJK ranges: \u4e00-\u9fff (CJK Unified Ideographs), \u3000-\u303f (CJK symbols),
|
|
59
|
+
# \u3400-\u4dbf (CJK Extension A), \uff00-\uffef (Fullwidth forms)
|
|
60
|
+
has_cjk_chars = any(
|
|
61
|
+
"\u4e00" <= char <= "\u9fff"
|
|
62
|
+
or "\u3000" <= char <= "\u303f"
|
|
63
|
+
or "\u3400" <= char <= "\u4dbf"
|
|
64
|
+
or "\uff00" <= char <= "\uffef"
|
|
65
|
+
for char in base
|
|
66
|
+
)
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# If decomposition produced multiple characters and first one is ASCII
|
|
81
|
-
if len(decomposed) > 1 and ord(decomposed[0]) < 128:
|
|
82
|
-
# Keep only the base character
|
|
83
|
-
result += decomposed[0].lower()
|
|
84
|
-
else:
|
|
85
|
-
# For non-Latin scripts like Chinese, preserve the character
|
|
68
|
+
if has_cjk_chars:
|
|
69
|
+
# For text with CJK characters, selectively transliterate only Latin accented chars
|
|
70
|
+
result = ""
|
|
71
|
+
for char in base:
|
|
72
|
+
if (
|
|
73
|
+
"\u4e00" <= char <= "\u9fff"
|
|
74
|
+
or "\u3000" <= char <= "\u303f"
|
|
75
|
+
or "\u3400" <= char <= "\u4dbf"
|
|
76
|
+
):
|
|
77
|
+
# Preserve CJK ideographs and symbols
|
|
86
78
|
result += char
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
elif "\uff00" <= char <= "\uffef":
|
|
80
|
+
# Remove Chinese fullwidth punctuation entirely (like ,!?)
|
|
81
|
+
continue
|
|
82
|
+
else:
|
|
83
|
+
# Transliterate Latin accented characters to ASCII
|
|
84
|
+
result += unidecode(char)
|
|
90
85
|
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
# Insert hyphens between CJK and Latin character transitions
|
|
87
|
+
# Match: CJK followed by Latin letter/digit, or Latin letter/digit followed by CJK
|
|
88
|
+
result = re.sub(
|
|
89
|
+
r"([\u4e00-\u9fff\u3000-\u303f\u3400-\u4dbf])([a-zA-Z0-9])", r"\1-\2", result
|
|
90
|
+
)
|
|
91
|
+
result = re.sub(
|
|
92
|
+
r"([a-zA-Z0-9])([\u4e00-\u9fff\u3000-\u303f\u3400-\u4dbf])", r"\1-\2", result
|
|
93
|
+
)
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
result = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", result)
|
|
95
|
+
# Insert dash between camelCase
|
|
96
|
+
result = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", result)
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
result = re.sub(r"([\u4e00-\u9fff])([a-zA-Z])", r"\1-\2", result)
|
|
101
|
-
result = re.sub(r"([a-zA-Z])([\u4e00-\u9fff])", r"\1-\2", result)
|
|
98
|
+
# Convert ASCII letters to lowercase, preserve CJK
|
|
99
|
+
lower_text = "".join(c.lower() if c.isascii() and c.isalpha() else c for c in result)
|
|
102
100
|
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
# Replace underscores with hyphens
|
|
102
|
+
text_with_hyphens = lower_text.replace("_", "-")
|
|
105
103
|
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
# Remove apostrophes entirely (don't replace with hyphens)
|
|
105
|
+
text_no_apostrophes = text_with_hyphens.replace("'", "")
|
|
108
106
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
# Replace unsafe chars with hyphens, but preserve CJK characters
|
|
108
|
+
clean_text = re.sub(
|
|
109
|
+
r"[^a-z0-9\u4e00-\u9fff\u3000-\u303f\u3400-\u4dbf/\-]", "-", text_no_apostrophes
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
# Original ASCII-only processing for backward compatibility
|
|
113
|
+
# Transliterate unicode to ascii
|
|
114
|
+
ascii_text = unidecode(base)
|
|
115
|
+
|
|
116
|
+
# Insert dash between camelCase
|
|
117
|
+
ascii_text = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", ascii_text)
|
|
118
|
+
|
|
119
|
+
# Convert to lowercase
|
|
120
|
+
lower_text = ascii_text.lower()
|
|
121
|
+
|
|
122
|
+
# replace underscores with hyphens
|
|
123
|
+
text_with_hyphens = lower_text.replace("_", "-")
|
|
124
|
+
|
|
125
|
+
# Remove apostrophes entirely (don't replace with hyphens)
|
|
126
|
+
text_no_apostrophes = text_with_hyphens.replace("'", "")
|
|
127
|
+
|
|
128
|
+
# Replace remaining invalid chars with hyphens
|
|
129
|
+
clean_text = re.sub(r"[^a-z0-9/\-]", "-", text_no_apostrophes)
|
|
114
130
|
|
|
115
131
|
# Collapse multiple hyphens
|
|
116
132
|
clean_text = re.sub(r"-+", "-", clean_text)
|
|
117
133
|
|
|
118
|
-
# Remove hyphens between adjacent Chinese characters only
|
|
119
|
-
# This handles cases like "你好-世界" -> "你好世界"
|
|
120
|
-
clean_text = re.sub(r"([\u4e00-\u9fff])-([\u4e00-\u9fff])", r"\1\2", clean_text)
|
|
121
|
-
|
|
122
134
|
# Clean each path segment
|
|
123
135
|
segments = clean_text.split("/")
|
|
124
136
|
clean_segments = [s.strip("-") for s in segments]
|
|
125
137
|
|
|
126
|
-
|
|
138
|
+
return_val = "/".join(clean_segments)
|
|
139
|
+
|
|
140
|
+
# Append file extension back, if necessary
|
|
141
|
+
if not split_extension and extension:
|
|
142
|
+
return_val += extension
|
|
143
|
+
|
|
144
|
+
return return_val
|
|
127
145
|
|
|
128
146
|
|
|
129
147
|
def setup_logging(
|
|
@@ -173,8 +191,6 @@ def setup_logging(
|
|
|
173
191
|
"httpx": logging.WARNING,
|
|
174
192
|
# File watching logs
|
|
175
193
|
"watchfiles.main": logging.WARNING,
|
|
176
|
-
# SQLAlchemy deprecation warnings
|
|
177
|
-
"sqlalchemy": logging.WARNING,
|
|
178
194
|
}
|
|
179
195
|
|
|
180
196
|
# Set log levels for noisy loggers
|
|
@@ -203,8 +219,21 @@ def parse_tags(tags: Union[List[str], str, None]) -> List[str]:
|
|
|
203
219
|
# First strip whitespace, then strip leading '#' characters to prevent accumulation
|
|
204
220
|
return [tag.strip().lstrip("#") for tag in tags if tag and tag.strip()]
|
|
205
221
|
|
|
206
|
-
# Process
|
|
222
|
+
# Process string input
|
|
207
223
|
if isinstance(tags, str):
|
|
224
|
+
# Check if it's a JSON array string (common issue from AI assistants)
|
|
225
|
+
import json
|
|
226
|
+
if tags.strip().startswith('[') and tags.strip().endswith(']'):
|
|
227
|
+
try:
|
|
228
|
+
# Try to parse as JSON array
|
|
229
|
+
parsed_json = json.loads(tags)
|
|
230
|
+
if isinstance(parsed_json, list):
|
|
231
|
+
# Recursively parse the JSON array as a list
|
|
232
|
+
return parse_tags(parsed_json)
|
|
233
|
+
except json.JSONDecodeError:
|
|
234
|
+
# Not valid JSON, fall through to comma-separated parsing
|
|
235
|
+
pass
|
|
236
|
+
|
|
208
237
|
# Split by comma, strip whitespace, then strip leading '#' characters
|
|
209
238
|
return [tag.strip().lstrip("#") for tag in tags.split(",") if tag and tag.strip()]
|
|
210
239
|
|
|
@@ -214,3 +243,137 @@ def parse_tags(tags: Union[List[str], str, None]) -> List[str]:
|
|
|
214
243
|
except (ValueError, TypeError): # pragma: no cover
|
|
215
244
|
logger.warning(f"Couldn't parse tags from input of type {type(tags)}: {tags}")
|
|
216
245
|
return []
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def normalize_newlines(multiline: str) -> str:
|
|
249
|
+
"""Replace any \r\n, \r, or \n with the native newline.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
multiline: String containing any mixture of newlines.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
A string with normalized newlines native to the platform.
|
|
256
|
+
"""
|
|
257
|
+
return re.sub(r"\r\n?|\n", os.linesep, multiline)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def normalize_file_path_for_comparison(file_path: str) -> str:
|
|
261
|
+
"""Normalize a file path for conflict detection.
|
|
262
|
+
|
|
263
|
+
This function normalizes file paths to help detect potential conflicts:
|
|
264
|
+
- Converts to lowercase for case-insensitive comparison
|
|
265
|
+
- Normalizes Unicode characters
|
|
266
|
+
- Handles path separators consistently
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
file_path: The file path to normalize
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Normalized file path for comparison purposes
|
|
273
|
+
"""
|
|
274
|
+
import unicodedata
|
|
275
|
+
|
|
276
|
+
# Convert to lowercase for case-insensitive comparison
|
|
277
|
+
normalized = file_path.lower()
|
|
278
|
+
|
|
279
|
+
# Normalize Unicode characters (NFD normalization)
|
|
280
|
+
normalized = unicodedata.normalize("NFD", normalized)
|
|
281
|
+
|
|
282
|
+
# Replace path separators with forward slashes
|
|
283
|
+
normalized = normalized.replace("\\", "/")
|
|
284
|
+
|
|
285
|
+
# Remove multiple slashes
|
|
286
|
+
normalized = re.sub(r"/+", "/", normalized)
|
|
287
|
+
|
|
288
|
+
return normalized
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def detect_potential_file_conflicts(file_path: str, existing_paths: List[str]) -> List[str]:
|
|
292
|
+
"""Detect potential conflicts between a file path and existing paths.
|
|
293
|
+
|
|
294
|
+
This function checks for various types of conflicts:
|
|
295
|
+
- Case sensitivity differences
|
|
296
|
+
- Unicode normalization differences
|
|
297
|
+
- Path separator differences
|
|
298
|
+
- Permalink generation conflicts
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
file_path: The file path to check
|
|
302
|
+
existing_paths: List of existing file paths to check against
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
List of existing paths that might conflict with the given file path
|
|
306
|
+
"""
|
|
307
|
+
conflicts = []
|
|
308
|
+
|
|
309
|
+
# Normalize the input file path
|
|
310
|
+
normalized_input = normalize_file_path_for_comparison(file_path)
|
|
311
|
+
input_permalink = generate_permalink(file_path)
|
|
312
|
+
|
|
313
|
+
for existing_path in existing_paths:
|
|
314
|
+
# Skip identical paths
|
|
315
|
+
if existing_path == file_path:
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
# Check for case-insensitive path conflicts
|
|
319
|
+
normalized_existing = normalize_file_path_for_comparison(existing_path)
|
|
320
|
+
if normalized_input == normalized_existing:
|
|
321
|
+
conflicts.append(existing_path)
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
# Check for permalink conflicts
|
|
325
|
+
existing_permalink = generate_permalink(existing_path)
|
|
326
|
+
if input_permalink == existing_permalink:
|
|
327
|
+
conflicts.append(existing_path)
|
|
328
|
+
continue
|
|
329
|
+
|
|
330
|
+
return conflicts
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def validate_project_path(path: str, project_path: Path) -> bool:
|
|
334
|
+
"""Ensure path stays within project boundaries."""
|
|
335
|
+
# Allow empty strings as they resolve to the project root
|
|
336
|
+
if not path:
|
|
337
|
+
return True
|
|
338
|
+
|
|
339
|
+
# Check for obvious path traversal patterns first
|
|
340
|
+
if ".." in path or "~" in path:
|
|
341
|
+
return False
|
|
342
|
+
|
|
343
|
+
# Check for Windows-style path traversal (even on Unix systems)
|
|
344
|
+
if "\\.." in path or path.startswith("\\"):
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
# Block absolute paths (Unix-style starting with / or Windows-style with drive letters)
|
|
348
|
+
if path.startswith("/") or (len(path) >= 2 and path[1] == ":"):
|
|
349
|
+
return False
|
|
350
|
+
|
|
351
|
+
# Block paths with control characters (but allow whitespace that will be stripped)
|
|
352
|
+
if path.strip() and any(ord(c) < 32 and c not in [" ", "\t"] for c in path):
|
|
353
|
+
return False
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
resolved = (project_path / path).resolve()
|
|
357
|
+
return resolved.is_relative_to(project_path.resolve())
|
|
358
|
+
except (ValueError, OSError):
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def ensure_timezone_aware(dt: datetime) -> datetime:
|
|
363
|
+
"""Ensure a datetime is timezone-aware using system timezone.
|
|
364
|
+
|
|
365
|
+
If the datetime is naive, convert it to timezone-aware using the system's local timezone.
|
|
366
|
+
If it's already timezone-aware, return it unchanged.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
dt: The datetime to ensure is timezone-aware
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
A timezone-aware datetime
|
|
373
|
+
"""
|
|
374
|
+
if dt.tzinfo is None:
|
|
375
|
+
# Naive datetime - assume it's in local time and add timezone
|
|
376
|
+
return dt.astimezone()
|
|
377
|
+
else:
|
|
378
|
+
# Already timezone-aware
|
|
379
|
+
return dt
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: basic-memory
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.4
|
|
4
4
|
Summary: Local-first knowledge management combining Zettelkasten with knowledge graphs
|
|
5
5
|
Project-URL: Homepage, https://github.com/basicmachines-co/basic-memory
|
|
6
6
|
Project-URL: Repository, https://github.com/basicmachines-co/basic-memory
|
|
@@ -13,7 +13,7 @@ Requires-Dist: aiosqlite>=0.20.0
|
|
|
13
13
|
Requires-Dist: alembic>=1.14.1
|
|
14
14
|
Requires-Dist: dateparser>=1.2.0
|
|
15
15
|
Requires-Dist: fastapi[standard]>=0.115.8
|
|
16
|
-
Requires-Dist: fastmcp
|
|
16
|
+
Requires-Dist: fastmcp==2.10.2
|
|
17
17
|
Requires-Dist: greenlet>=3.1.1
|
|
18
18
|
Requires-Dist: icecream>=2.1.3
|
|
19
19
|
Requires-Dist: loguru>=0.7.3
|
|
@@ -51,11 +51,8 @@ Basic Memory lets you build persistent knowledge through natural conversations w
|
|
|
51
51
|
Claude, while keeping everything in simple Markdown files on your computer. It uses the Model Context Protocol (MCP) to
|
|
52
52
|
enable any compatible LLM to read and write to your local knowledge base.
|
|
53
53
|
|
|
54
|
-
- Website: https://
|
|
55
|
-
- Company: https://basicmachines.co
|
|
54
|
+
- Website: https://basicmachines.co
|
|
56
55
|
- Documentation: https://memory.basicmachines.co
|
|
57
|
-
- Discord: https://discord.gg/tyvKNccgqN
|
|
58
|
-
- YouTube: https://www.youtube.com/@basicmachines-co
|
|
59
56
|
|
|
60
57
|
## Pick up your conversation right where you left off
|
|
61
58
|
|
|
@@ -71,10 +68,6 @@ https://github.com/user-attachments/assets/a55d8238-8dd0-454a-be4c-8860dbbd0ddc
|
|
|
71
68
|
# Install with uv (recommended)
|
|
72
69
|
uv tool install basic-memory
|
|
73
70
|
|
|
74
|
-
# or with Homebrew
|
|
75
|
-
brew tap basicmachines-co/basic-memory
|
|
76
|
-
brew install basic-memory
|
|
77
|
-
|
|
78
71
|
# Configure Claude Desktop (edit ~/Library/Application Support/Claude/claude_desktop_config.json)
|
|
79
72
|
# Add this to your config:
|
|
80
73
|
{
|
|
@@ -106,14 +99,8 @@ Memory for Claude Desktop:
|
|
|
106
99
|
npx -y @smithery/cli install @basicmachines-co/basic-memory --client claude
|
|
107
100
|
```
|
|
108
101
|
|
|
109
|
-
This installs and configures Basic Memory without requiring manual edits to the Claude Desktop configuration file.
|
|
110
|
-
|
|
111
|
-
### Add to Cursor
|
|
112
|
-
|
|
113
|
-
Once you have installed Basic Memory revisit this page for the 1-click installer for Cursor:
|
|
114
|
-
|
|
115
|
-
[](https://cursor.com/install-mcp?name=basic-memory&config=eyJjb21tYW5kIjoiL1VzZXJzL2RyZXcvLmxvY2FsL2Jpbi91dnggYmFzaWMtbWVtb3J5IG1jcCJ9)
|
|
116
|
-
|
|
102
|
+
This installs and configures Basic Memory without requiring manual edits to the Claude Desktop configuration file. The
|
|
103
|
+
Smithery server hosts the MCP server component, while your data remains stored locally as Markdown files.
|
|
117
104
|
|
|
118
105
|
### Glama.ai
|
|
119
106
|
|
|
@@ -204,8 +191,7 @@ The note embeds semantic content and links to other topics via simple Markdown f
|
|
|
204
191
|
|
|
205
192
|
3. You see this file on your computer in real time in the current project directory (default `~/$HOME/basic-memory`).
|
|
206
193
|
|
|
207
|
-
- Realtime sync
|
|
208
|
-
- Project switching during conversations is supported starting with v0.13.0
|
|
194
|
+
- Realtime sync can be enabled via running `basic-memory sync --watch`
|
|
209
195
|
|
|
210
196
|
4. In a chat with the LLM, you can reference a topic:
|
|
211
197
|
|
|
@@ -263,7 +249,7 @@ title: <Entity title>
|
|
|
263
249
|
type: <The type of Entity> (e.g. note)
|
|
264
250
|
permalink: <a uri slug>
|
|
265
251
|
|
|
266
|
-
- <optional metadata> (such as tags)
|
|
252
|
+
- <optional metadata> (such as tags)
|
|
267
253
|
```
|
|
268
254
|
|
|
269
255
|
### Observations
|
|
@@ -315,13 +301,6 @@ Examples of relations:
|
|
|
315
301
|
```
|
|
316
302
|
|
|
317
303
|
## Using with VS Code
|
|
318
|
-
For one-click installation, click one of the install buttons below...
|
|
319
|
-
|
|
320
|
-
[](https://insiders.vscode.dev/redirect/mcp/install?name=basic-memory&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22basic-memory%22%2C%22mcp%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=basic-memory&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22basic-memory%22%2C%22mcp%22%5D%7D&quality=insiders)
|
|
321
|
-
|
|
322
|
-
You can use Basic Memory with VS Code to easily retrieve and store information while coding. Click the installation buttons above for one-click setup, or follow the manual installation instructions below.
|
|
323
|
-
|
|
324
|
-
### Manual Installation
|
|
325
304
|
|
|
326
305
|
Add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
|
|
327
306
|
|
|
@@ -351,6 +330,8 @@ Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace
|
|
|
351
330
|
}
|
|
352
331
|
```
|
|
353
332
|
|
|
333
|
+
You can use Basic Memory with VS Code to easily retrieve and store information while coding.
|
|
334
|
+
|
|
354
335
|
## Using with Claude Desktop
|
|
355
336
|
|
|
356
337
|
Basic Memory is built using the MCP (Model Context Protocol) and works with the Claude desktop app (https://claude.ai/):
|
|
@@ -374,8 +355,7 @@ for OS X):
|
|
|
374
355
|
}
|
|
375
356
|
```
|
|
376
357
|
|
|
377
|
-
If you want to use a specific project (see [Multiple Projects](
|
|
378
|
-
Claude Desktop
|
|
358
|
+
If you want to use a specific project (see [Multiple Projects](#multiple-projects) below), update your Claude Desktop
|
|
379
359
|
config:
|
|
380
360
|
|
|
381
361
|
```json
|
|
@@ -385,9 +365,9 @@ config:
|
|
|
385
365
|
"command": "uvx",
|
|
386
366
|
"args": [
|
|
387
367
|
"basic-memory",
|
|
368
|
+
"mcp",
|
|
388
369
|
"--project",
|
|
389
|
-
"your-project-name"
|
|
390
|
-
"mcp"
|
|
370
|
+
"your-project-name"
|
|
391
371
|
]
|
|
392
372
|
}
|
|
393
373
|
}
|
|
@@ -396,27 +376,23 @@ config:
|
|
|
396
376
|
|
|
397
377
|
2. Sync your knowledge:
|
|
398
378
|
|
|
399
|
-
|
|
379
|
+
```bash
|
|
380
|
+
# One-time sync of local knowledge updates
|
|
381
|
+
basic-memory sync
|
|
382
|
+
|
|
383
|
+
# Run realtime sync process (recommended)
|
|
384
|
+
basic-memory sync --watch
|
|
385
|
+
```
|
|
400
386
|
|
|
401
387
|
3. In Claude Desktop, the LLM can now use these tools:
|
|
402
388
|
|
|
403
389
|
```
|
|
404
390
|
write_note(title, content, folder, tags) - Create or update notes
|
|
405
391
|
read_note(identifier, page, page_size) - Read notes by title or permalink
|
|
406
|
-
edit_note(identifier, operation, content) - Edit notes incrementally (append, prepend, find/replace)
|
|
407
|
-
move_note(identifier, destination_path) - Move notes with database consistency
|
|
408
|
-
view_note(identifier) - Display notes as formatted artifacts for better readability
|
|
409
392
|
build_context(url, depth, timeframe) - Navigate knowledge graph via memory:// URLs
|
|
410
|
-
|
|
393
|
+
search(query, page, page_size) - Search across your knowledge base
|
|
411
394
|
recent_activity(type, depth, timeframe) - Find recently updated information
|
|
412
395
|
canvas(nodes, edges, title, folder) - Generate knowledge visualizations
|
|
413
|
-
list_memory_projects() - List all available projects with status
|
|
414
|
-
switch_project(project_name) - Switch to different project context
|
|
415
|
-
get_current_project() - Show current project and statistics
|
|
416
|
-
create_memory_project(name, path, set_default) - Create new projects
|
|
417
|
-
delete_project(name) - Delete projects from configuration
|
|
418
|
-
set_default_project(name) - Set default project
|
|
419
|
-
sync_status() - Check file synchronization status
|
|
420
396
|
```
|
|
421
397
|
|
|
422
398
|
5. Example prompts to try:
|
|
@@ -427,10 +403,6 @@ sync_status() - Check file synchronization status
|
|
|
427
403
|
"Create a canvas visualization of my project components"
|
|
428
404
|
"Read my notes on the authentication system"
|
|
429
405
|
"What have I been working on in the past week?"
|
|
430
|
-
"Switch to my work-notes project"
|
|
431
|
-
"List all my available projects"
|
|
432
|
-
"Edit my coffee brewing note to add a new technique"
|
|
433
|
-
"Move my old meeting notes to the archive folder"
|
|
434
406
|
```
|
|
435
407
|
|
|
436
408
|
## Futher info
|
|
@@ -442,49 +414,6 @@ See the [Documentation](https://memory.basicmachines.co/) for more info, includi
|
|
|
442
414
|
- [Managing multiple Projects](https://memory.basicmachines.co/docs/cli-reference#project)
|
|
443
415
|
- [Importing data from OpenAI/Claude Projects](https://memory.basicmachines.co/docs/cli-reference#import)
|
|
444
416
|
|
|
445
|
-
## Installation Options
|
|
446
|
-
|
|
447
|
-
### Stable Release
|
|
448
|
-
```bash
|
|
449
|
-
pip install basic-memory
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
### Beta/Pre-releases
|
|
453
|
-
```bash
|
|
454
|
-
pip install basic-memory --pre
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
### Development Builds
|
|
458
|
-
Development versions are automatically published on every commit to main with versions like `0.12.4.dev26+468a22f`:
|
|
459
|
-
```bash
|
|
460
|
-
pip install basic-memory --pre --force-reinstall
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
### Docker
|
|
464
|
-
|
|
465
|
-
Run Basic Memory in a container with volume mounting for your Obsidian vault:
|
|
466
|
-
|
|
467
|
-
```bash
|
|
468
|
-
# Clone and start with Docker Compose
|
|
469
|
-
git clone https://github.com/basicmachines-co/basic-memory.git
|
|
470
|
-
cd basic-memory
|
|
471
|
-
|
|
472
|
-
# Edit docker-compose.yml to point to your Obsidian vault
|
|
473
|
-
# Then start the container
|
|
474
|
-
docker-compose up -d
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
Or use Docker directly:
|
|
478
|
-
```bash
|
|
479
|
-
docker run -d \
|
|
480
|
-
--name basic-memory-server \
|
|
481
|
-
-v /path/to/your/obsidian-vault:/data/knowledge:rw \
|
|
482
|
-
-v basic-memory-config:/root/.basic-memory:rw \
|
|
483
|
-
ghcr.io/basicmachines-co/basic-memory:latest
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
See [Docker Setup Guide](docs/Docker.md) for detailed configuration options, multiple project setup, and integration examples.
|
|
487
|
-
|
|
488
417
|
## License
|
|
489
418
|
|
|
490
419
|
AGPL-3.0
|
|
@@ -502,4 +431,4 @@ and submitting PRs.
|
|
|
502
431
|
</picture>
|
|
503
432
|
</a>
|
|
504
433
|
|
|
505
|
-
Built with ♥️ by Basic Machines
|
|
434
|
+
Built with ♥️ by Basic Machines
|