basic-memory 0.14.3__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.

Files changed (34) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +53 -0
  3. basic_memory/api/routers/resource_router.py +3 -3
  4. basic_memory/cli/commands/project.py +9 -10
  5. basic_memory/config.py +20 -8
  6. basic_memory/file_utils.py +65 -0
  7. basic_memory/importers/chatgpt_importer.py +1 -1
  8. basic_memory/importers/utils.py +2 -2
  9. basic_memory/markdown/entity_parser.py +2 -2
  10. basic_memory/markdown/markdown_processor.py +2 -2
  11. basic_memory/markdown/plugins.py +42 -26
  12. basic_memory/markdown/utils.py +1 -1
  13. basic_memory/mcp/tools/build_context.py +12 -2
  14. basic_memory/mcp/tools/project_management.py +22 -7
  15. basic_memory/mcp/tools/read_note.py +16 -13
  16. basic_memory/models/knowledge.py +13 -2
  17. basic_memory/models/project.py +2 -2
  18. basic_memory/repository/entity_repository.py +2 -2
  19. basic_memory/repository/project_repository.py +1 -1
  20. basic_memory/repository/search_repository.py +7 -3
  21. basic_memory/schemas/base.py +40 -10
  22. basic_memory/schemas/memory.py +23 -11
  23. basic_memory/services/context_service.py +12 -2
  24. basic_memory/services/directory_service.py +7 -0
  25. basic_memory/services/entity_service.py +8 -8
  26. basic_memory/services/project_service.py +11 -11
  27. basic_memory/sync/sync_service.py +3 -3
  28. basic_memory/sync/watch_service.py +31 -8
  29. basic_memory/utils.py +169 -107
  30. {basic_memory-0.14.3.dist-info → basic_memory-0.14.4.dist-info}/METADATA +20 -91
  31. {basic_memory-0.14.3.dist-info → basic_memory-0.14.4.dist-info}/RECORD +34 -33
  32. {basic_memory-0.14.3.dist-info → basic_memory-0.14.4.dist-info}/WHEEL +0 -0
  33. {basic_memory-0.14.3.dist-info → basic_memory-0.14.4.dist-info}/entry_points.txt +0 -0
  34. {basic_memory-0.14.3.dist-info → basic_memory-0.14.4.dist-info}/licenses/LICENSE +0 -0
basic_memory/utils.py CHANGED
@@ -4,11 +4,13 @@ import os
4
4
 
5
5
  import logging
6
6
  import re
7
- import unicodedata
7
+ import sys
8
+ from datetime import datetime
8
9
  from pathlib import Path
9
- from typing import Optional, Protocol, Union, runtime_checkable, List, Any
10
+ from typing import Optional, Protocol, Union, runtime_checkable, List
10
11
 
11
12
  from loguru import logger
13
+ from unidecode import unidecode
12
14
 
13
15
 
14
16
  @runtime_checkable
@@ -26,9 +28,11 @@ FilePath = Union[Path, str]
26
28
  logging.getLogger("opentelemetry.sdk.metrics._internal.instrument").setLevel(logging.ERROR)
27
29
 
28
30
 
29
- def generate_permalink(file_path: Union[Path, str, Any]) -> str:
30
- """
31
- Generate a permalink from a file path.
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)
32
36
 
33
37
  Returns:
34
38
  Normalized permalink that matches validation rules. Converts spaces and underscores
@@ -37,7 +41,7 @@ def generate_permalink(file_path: Union[Path, str, Any]) -> str:
37
41
  Examples:
38
42
  >>> generate_permalink("docs/My Feature.md")
39
43
  'docs/my-feature'
40
- >>> generate_permalink("specs/API_v2.md")
44
+ >>> generate_permalink("specs/API (v2).md")
41
45
  'specs/api-v2'
42
46
  >>> generate_permalink("design/unified_model_refactor.md")
43
47
  'design/unified-model-refactor'
@@ -45,84 +49,99 @@ def generate_permalink(file_path: Union[Path, str, Any]) -> str:
45
49
  '中文/测试文档'
46
50
  """
47
51
  # Convert Path to string if needed
48
- path_str = str(file_path)
49
-
50
- # Remove extension
51
- base = os.path.splitext(path_str)[0]
52
-
53
- # Create a transliteration mapping for specific characters
54
- transliteration_map = {
55
- "ø": "o", # Handle Søren -> soren
56
- "å": "a", # Handle Kierkegård -> kierkegard
57
- "ü": "u", # Handle Müller -> muller
58
- "é": "e", # Handle Café -> cafe
59
- "è": "e", # Handle Mère -> mere
60
- "ê": "e", # Handle Fête -> fete
61
- "à": "a", # Handle À la mode -> a la mode
62
- "ç": "c", # Handle Façade -> facade
63
- "ñ": "n", # Handle Niño -> nino
64
- "ö": "o", # Handle Björk -> bjork
65
- "ä": "a", # Handle Häagen -> haagen
66
- # Add more mappings as needed
67
- }
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
+ )
68
67
 
69
- # Process character by character, transliterating Latin characters with diacritics
70
- result = ""
71
- for char in base:
72
- # Direct mapping for known characters
73
- if char.lower() in transliteration_map:
74
- result += transliteration_map[char.lower()]
75
- # General case using Unicode normalization
76
- elif unicodedata.category(char).startswith("L") and ord(char) > 127:
77
- # Decompose the character (e.g., ü -> u + combining diaeresis)
78
- decomposed = unicodedata.normalize("NFD", char)
79
- # If decomposition produced multiple characters and first one is ASCII
80
- if len(decomposed) > 1 and ord(decomposed[0]) < 128:
81
- # Keep only the base character
82
- result += decomposed[0].lower()
83
- else:
84
- # 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
85
78
  result += char
86
- else:
87
- # Add the character as is
88
- result += char
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)
89
85
 
90
- # Handle special punctuation cases for apostrophes
91
- result = result.replace("'", "")
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
+ )
92
94
 
93
- # Insert dash between camelCase
94
- # This regex finds boundaries between lowercase and uppercase letters
95
- 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)
96
97
 
97
- # Insert dash between Chinese and Latin character boundaries
98
- # This is needed for cases like "中文English" -> "中文-english"
99
- result = re.sub(r"([\u4e00-\u9fff])([a-zA-Z])", r"\1-\2", result)
100
- 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)
101
100
 
102
- # Convert ASCII letters to lowercase, preserve non-ASCII characters
103
- lower_text = "".join(c.lower() if c.isascii() and c.isalpha() else c for c in result)
101
+ # Replace underscores with hyphens
102
+ text_with_hyphens = lower_text.replace("_", "-")
104
103
 
105
- # Replace underscores with hyphens
106
- text_with_hyphens = lower_text.replace("_", "-")
104
+ # Remove apostrophes entirely (don't replace with hyphens)
105
+ text_no_apostrophes = text_with_hyphens.replace("'", "")
107
106
 
108
- # Replace spaces and unsafe ASCII characters with hyphens, but preserve non-ASCII characters
109
- # Include common Chinese character ranges and other non-ASCII characters
110
- clean_text = re.sub(
111
- r"[^a-z0-9\u4e00-\u9fff\u3000-\u303f\u3400-\u4dbf/\-]", "-", text_with_hyphens
112
- )
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)
113
130
 
114
131
  # Collapse multiple hyphens
115
132
  clean_text = re.sub(r"-+", "-", clean_text)
116
133
 
117
- # Remove hyphens between adjacent Chinese characters only
118
- # This handles cases like "你好-世界" -> "你好世界"
119
- clean_text = re.sub(r"([\u4e00-\u9fff])-([\u4e00-\u9fff])", r"\1\2", clean_text)
120
-
121
134
  # Clean each path segment
122
135
  segments = clean_text.split("/")
123
136
  clean_segments = [s.strip("-") for s in segments]
124
137
 
125
- return "/".join(clean_segments)
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
126
145
 
127
146
 
128
147
  def setup_logging(
@@ -143,7 +162,7 @@ def setup_logging(
143
162
  console: Whether to log to the console
144
163
  """
145
164
  # Remove default handler and any existing handlers
146
- # logger.remove()
165
+ logger.remove()
147
166
 
148
167
  # Add file handler if we are not running tests and a log file is specified
149
168
  if log_file and env != "test":
@@ -161,8 +180,8 @@ def setup_logging(
161
180
  )
162
181
 
163
182
  # Add console logger if requested or in test mode
164
- # if env == "test" or console:
165
- # logger.add(sys.stderr, level=log_level, backtrace=True, diagnose=True, colorize=True)
183
+ if env == "test" or console:
184
+ logger.add(sys.stderr, level=log_level, backtrace=True, diagnose=True, colorize=True)
166
185
 
167
186
  logger.info(f"ENV: '{env}' Log level: '{log_level}' Logging to {log_file}")
168
187
 
@@ -172,8 +191,6 @@ def setup_logging(
172
191
  "httpx": logging.WARNING,
173
192
  # File watching logs
174
193
  "watchfiles.main": logging.WARNING,
175
- # SQLAlchemy deprecation warnings
176
- "sqlalchemy": logging.WARNING,
177
194
  }
178
195
 
179
196
  # Set log levels for noisy loggers
@@ -181,6 +198,65 @@ def setup_logging(
181
198
  logging.getLogger(logger_name).setLevel(level)
182
199
 
183
200
 
201
+ def parse_tags(tags: Union[List[str], str, None]) -> List[str]:
202
+ """Parse tags from various input formats into a consistent list.
203
+
204
+ Args:
205
+ tags: Can be a list of strings, a comma-separated string, or None
206
+
207
+ Returns:
208
+ A list of tag strings, or an empty list if no tags
209
+
210
+ Note:
211
+ This function strips leading '#' characters from tags to prevent
212
+ their accumulation when tags are processed multiple times.
213
+ """
214
+ if tags is None:
215
+ return []
216
+
217
+ # Process list of tags
218
+ if isinstance(tags, list):
219
+ # First strip whitespace, then strip leading '#' characters to prevent accumulation
220
+ return [tag.strip().lstrip("#") for tag in tags if tag and tag.strip()]
221
+
222
+ # Process string input
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
+
237
+ # Split by comma, strip whitespace, then strip leading '#' characters
238
+ return [tag.strip().lstrip("#") for tag in tags.split(",") if tag and tag.strip()]
239
+
240
+ # For any other type, try to convert to string and parse
241
+ try: # pragma: no cover
242
+ return parse_tags(str(tags))
243
+ except (ValueError, TypeError): # pragma: no cover
244
+ logger.warning(f"Couldn't parse tags from input of type {type(tags)}: {tags}")
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
+
184
260
  def normalize_file_path_for_comparison(file_path: str) -> str:
185
261
  """Normalize a file path for conflict detection.
186
262
 
@@ -254,40 +330,6 @@ def detect_potential_file_conflicts(file_path: str, existing_paths: List[str]) -
254
330
  return conflicts
255
331
 
256
332
 
257
- def parse_tags(tags: Union[List[str], str, None]) -> List[str]:
258
- """Parse tags from various input formats into a consistent list.
259
-
260
- Args:
261
- tags: Can be a list of strings, a comma-separated string, or None
262
-
263
- Returns:
264
- A list of tag strings, or an empty list if no tags
265
-
266
- Note:
267
- This function strips leading '#' characters from tags to prevent
268
- their accumulation when tags are processed multiple times.
269
- """
270
- if tags is None:
271
- return []
272
-
273
- # Process list of tags
274
- if isinstance(tags, list):
275
- # First strip whitespace, then strip leading '#' characters to prevent accumulation
276
- return [tag.strip().lstrip("#") for tag in tags if tag and tag.strip()]
277
-
278
- # Process comma-separated string of tags
279
- if isinstance(tags, str):
280
- # Split by comma, strip whitespace, then strip leading '#' characters
281
- return [tag.strip().lstrip("#") for tag in tags.split(",") if tag and tag.strip()]
282
-
283
- # For any other type, try to convert to string and parse
284
- try: # pragma: no cover
285
- return parse_tags(str(tags))
286
- except (ValueError, TypeError): # pragma: no cover
287
- logger.warning(f"Couldn't parse tags from input of type {type(tags)}: {tags}")
288
- return []
289
-
290
-
291
333
  def validate_project_path(path: str, project_path: Path) -> bool:
292
334
  """Ensure path stays within project boundaries."""
293
335
  # Allow empty strings as they resolve to the project root
@@ -315,3 +357,23 @@ def validate_project_path(path: str, project_path: Path) -> bool:
315
357
  return resolved.is_relative_to(project_path.resolve())
316
358
  except (ValueError, OSError):
317
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
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
@@ -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://basicmemory.com
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. Note: The Smithery installation uses their hosted MCP server, while your data remains stored locally as Markdown files.
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
- [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](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 is enabled by default starting with v0.12.0
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
- [![Install with UV in VS Code](https://img.shields.io/badge/VS_Code-UV-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](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) [![Install with UV in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-UV-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](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](docs/User%20Guide.md#multiple-projects)), update your
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
- Basic Memory will sync the files in your project in real time if you make manual edits.
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
- search_notes(query, page, page_size) - Search across your knowledge base
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