basic-memory 0.9.0__py3-none-any.whl → 0.10.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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
2
2
 
3
- __version__ = "0.9.0"
3
+ __version__ = "0.10.0"
@@ -3,9 +3,6 @@
3
3
  import json
4
4
  from datetime import datetime
5
5
 
6
- from fastapi import APIRouter
7
- from sqlalchemy import text
8
-
9
6
  from basic_memory.config import config, config_manager
10
7
  from basic_memory.deps import (
11
8
  ProjectInfoRepositoryDep,
@@ -18,6 +15,8 @@ from basic_memory.schemas import (
18
15
  SystemStatus,
19
16
  )
20
17
  from basic_memory.sync.watch_service import WATCH_STATUS_JSON
18
+ from fastapi import APIRouter
19
+ from sqlalchemy import text
21
20
 
22
21
  router = APIRouter(prefix="/stats", tags=["statistics"])
23
22
 
@@ -262,7 +261,7 @@ async def get_system_status() -> SystemStatus:
262
261
  watch_status_path = config.home / ".basic-memory" / WATCH_STATUS_JSON
263
262
  if watch_status_path.exists():
264
263
  try:
265
- watch_status = json.loads(watch_status_path.read_text())
264
+ watch_status = json.loads(watch_status_path.read_text(encoding="utf-8"))
266
265
  except Exception: # pragma: no cover
267
266
  pass
268
267
 
@@ -7,15 +7,14 @@ from pathlib import Path
7
7
  from typing import Dict, Any, List, Annotated, Set, Optional
8
8
 
9
9
  import typer
10
- from loguru import logger
11
- from rich.console import Console
12
- from rich.panel import Panel
13
- from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
14
-
15
10
  from basic_memory.cli.app import import_app
16
11
  from basic_memory.config import config
17
12
  from basic_memory.markdown import EntityParser, MarkdownProcessor
18
13
  from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
14
+ from loguru import logger
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
19
18
 
20
19
  console = Console()
21
20
 
@@ -167,7 +166,7 @@ async def process_chatgpt_json(
167
166
  read_task = progress.add_task("Reading chat data...", total=None)
168
167
 
169
168
  # Read conversations
170
- conversations = json.loads(json_path.read_text())
169
+ conversations = json.loads(json_path.read_text(encoding="utf-8"))
171
170
  progress.update(read_task, total=len(conversations))
172
171
 
173
172
  # Process each conversation
@@ -7,15 +7,14 @@ from pathlib import Path
7
7
  from typing import Dict, Any, List, Annotated
8
8
 
9
9
  import typer
10
- from loguru import logger
11
- from rich.console import Console
12
- from rich.panel import Panel
13
- from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
14
-
15
10
  from basic_memory.cli.app import claude_app
16
11
  from basic_memory.config import config
17
12
  from basic_memory.markdown import EntityParser, MarkdownProcessor
18
13
  from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
14
+ from loguru import logger
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
19
18
 
20
19
  console = Console()
21
20
 
@@ -124,7 +123,7 @@ async def process_conversations_json(
124
123
  read_task = progress.add_task("Reading chat data...", total=None)
125
124
 
126
125
  # Read chat data - handle array of arrays format
127
- data = json.loads(json_path.read_text())
126
+ data = json.loads(json_path.read_text(encoding="utf-8"))
128
127
  conversations = [chat for chat in data]
129
128
  progress.update(read_task, total=len(conversations))
130
129
 
@@ -6,15 +6,14 @@ from pathlib import Path
6
6
  from typing import Dict, Any, Annotated, Optional
7
7
 
8
8
  import typer
9
- from loguru import logger
10
- from rich.console import Console
11
- from rich.panel import Panel
12
- from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
13
-
14
9
  from basic_memory.cli.app import claude_app
15
10
  from basic_memory.config import config
16
11
  from basic_memory.markdown import EntityParser, MarkdownProcessor
17
12
  from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
13
+ from loguru import logger
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
18
17
 
19
18
  console = Console()
20
19
 
@@ -103,7 +102,7 @@ async def process_projects_json(
103
102
  read_task = progress.add_task("Reading project data...", total=None)
104
103
 
105
104
  # Read project data
106
- data = json.loads(json_path.read_text())
105
+ data = json.loads(json_path.read_text(encoding="utf-8"))
107
106
  progress.update(read_task, total=len(data))
108
107
 
109
108
  # Track import counts
basic_memory/config.py CHANGED
@@ -5,13 +5,12 @@ import os
5
5
  from pathlib import Path
6
6
  from typing import Any, Dict, Literal, Optional
7
7
 
8
+ import basic_memory
9
+ from basic_memory.utils import setup_logging
8
10
  from loguru import logger
9
11
  from pydantic import Field, field_validator
10
12
  from pydantic_settings import BaseSettings, SettingsConfigDict
11
13
 
12
- import basic_memory
13
- from basic_memory.utils import setup_logging
14
-
15
14
  DATABASE_NAME = "memory.db"
16
15
  DATA_DIR_NAME = ".basic-memory"
17
16
  CONFIG_FILE_NAME = "config.json"
@@ -111,7 +110,7 @@ class ConfigManager:
111
110
  """Load configuration from file or create default."""
112
111
  if self.config_file.exists():
113
112
  try:
114
- data = json.loads(self.config_file.read_text())
113
+ data = json.loads(self.config_file.read_text(encoding="utf-8"))
115
114
  return BasicMemoryConfig(**data)
116
115
  except Exception as e:
117
116
  logger.error(f"Failed to load config: {e}")
@@ -85,7 +85,7 @@ async def write_file_atomic(path: FilePath, content: str) -> None:
85
85
  temp_path = path_obj.with_suffix(".tmp")
86
86
 
87
87
  try:
88
- temp_path.write_text(content)
88
+ temp_path.write_text(content, encoding="utf-8")
89
89
  temp_path.replace(path_obj)
90
90
  logger.debug("Wrote file atomically", path=str(path_obj), content_length=len(content))
91
91
  except Exception as e: # pragma: no cover
@@ -203,7 +203,7 @@ async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
203
203
  path_obj = Path(path) if isinstance(path, str) else path
204
204
 
205
205
  # Read current content
206
- content = path_obj.read_text()
206
+ content = path_obj.read_text(encoding="utf-8")
207
207
 
208
208
  # Parse current frontmatter
209
209
  current_fm = {}
@@ -215,7 +215,7 @@ async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
215
215
  new_fm = {**current_fm, **updates}
216
216
 
217
217
  # Write new file with updated frontmatter
218
- yaml_fm = yaml.dump(new_fm, sort_keys=False)
218
+ yaml_fm = yaml.dump(new_fm, sort_keys=False, allow_unicode=True)
219
219
  final_content = f"---\n{yaml_fm}---\n\n{content.strip()}"
220
220
 
221
221
  logger.debug("Updating frontmatter", path=str(path_obj), update_keys=list(updates.keys()))
@@ -83,7 +83,7 @@ class MarkdownProcessor:
83
83
  """
84
84
  # Dirty check if needed
85
85
  if expected_checksum is not None:
86
- current_content = path.read_text()
86
+ current_content = path.read_text(encoding="utf-8")
87
87
  current_checksum = await file_utils.compute_checksum(current_content)
88
88
  if current_checksum != expected_checksum:
89
89
  raise DirtyFileError(f"File {path} has been modified")
@@ -1,8 +1,7 @@
1
1
  from pathlib import Path
2
2
 
3
- from loguru import logger
4
-
5
3
  from basic_memory.mcp.server import mcp
4
+ from loguru import logger
6
5
 
7
6
 
8
7
  @mcp.resource(
@@ -20,7 +19,9 @@ def ai_assistant_guide() -> str:
20
19
  A focused guide on Basic Memory usage.
21
20
  """
22
21
  logger.info("Loading AI assistant guide resource")
23
- guide_doc = Path(__file__).parent.parent.parent.parent.parent / "static" / "ai_assistant_guide.md"
24
- content = guide_doc.read_text()
22
+ guide_doc = (
23
+ Path(__file__).parent.parent.parent.parent.parent / "static" / "ai_assistant_guide.md"
24
+ )
25
+ content = guide_doc.read_text(encoding="utf-8")
25
26
  logger.info(f"Loaded AI assistant guide ({len(content)} chars)")
26
27
  return content
@@ -5,8 +5,6 @@ from os import stat_result
5
5
  from pathlib import Path
6
6
  from typing import Any, Dict, Tuple, Union
7
7
 
8
- from loguru import logger
9
-
10
8
  from basic_memory import file_utils
11
9
  from basic_memory.file_utils import FileError
12
10
  from basic_memory.markdown.markdown_processor import MarkdownProcessor
@@ -14,6 +12,7 @@ from basic_memory.models import Entity as EntityModel
14
12
  from basic_memory.schemas import Entity as EntitySchema
15
13
  from basic_memory.services.exceptions import FileOperationError
16
14
  from basic_memory.utils import FilePath
15
+ from loguru import logger
17
16
 
18
17
 
19
18
  class FileService:
@@ -171,8 +170,7 @@ class FileService:
171
170
 
172
171
  try:
173
172
  logger.debug("Reading file", operation="read_file", path=str(full_path))
174
-
175
- content = full_path.read_text()
173
+ content = full_path.read_text(encoding="utf-8")
176
174
  checksum = await file_utils.compute_checksum(content)
177
175
 
178
176
  logger.debug(
@@ -236,7 +234,7 @@ class FileService:
236
234
  try:
237
235
  if self.is_markdown(path):
238
236
  # read str
239
- content = full_path.read_text()
237
+ content = full_path.read_text(encoding="utf-8")
240
238
  else:
241
239
  # read bytes
242
240
  content = full_path.read_bytes()
@@ -5,16 +5,15 @@ from datetime import datetime
5
5
  from pathlib import Path
6
6
  from typing import List, Optional, Set
7
7
 
8
+ from basic_memory.config import ProjectConfig
9
+ from basic_memory.services.file_service import FileService
10
+ from basic_memory.sync.sync_service import SyncService
8
11
  from loguru import logger
9
12
  from pydantic import BaseModel
10
13
  from rich.console import Console
11
14
  from watchfiles import awatch
12
15
  from watchfiles.main import FileChange, Change
13
16
 
14
- from basic_memory.config import ProjectConfig
15
- from basic_memory.services.file_service import FileService
16
- from basic_memory.sync.sync_service import SyncService
17
-
18
17
  WATCH_STATUS_JSON = "watch-status.json"
19
18
 
20
19
 
@@ -138,6 +137,10 @@ class WatchService:
138
137
  if part.startswith("."):
139
138
  return False
140
139
 
140
+ # Skip temp files used in atomic operations
141
+ if path.endswith(".tmp"):
142
+ return False
143
+
141
144
  return True
142
145
 
143
146
  async def write_status(self):
@@ -161,6 +164,11 @@ class WatchService:
161
164
  for change, path in changes:
162
165
  # convert to relative path
163
166
  relative_path = str(Path(path).relative_to(directory))
167
+
168
+ # Skip .tmp files - they're temporary and shouldn't be synced
169
+ if relative_path.endswith(".tmp"):
170
+ continue
171
+
164
172
  if change == Change.added:
165
173
  adds.append(relative_path)
166
174
  elif change == Change.deleted:
@@ -184,8 +192,8 @@ class WatchService:
184
192
  # We don't need to process directories, only the files inside them
185
193
  # This prevents errors when trying to compute checksums or read directories as files
186
194
  added_full_path = directory / added_path
187
- if added_full_path.is_dir():
188
- logger.debug("Skipping directory for move detection", path=added_path)
195
+ if not added_full_path.exists() or added_full_path.is_dir():
196
+ logger.debug("Skipping non-existent or directory path", path=added_path)
189
197
  processed.add(added_path)
190
198
  continue
191
199
 
@@ -247,10 +255,12 @@ class WatchService:
247
255
  if path not in processed:
248
256
  # Skip directories - only process files
249
257
  full_path = directory / path
250
- if full_path.is_dir(): # pragma: no cover
251
- logger.debug("Skipping directory", path=path)
252
- processed.add(path)
253
- continue
258
+ if not full_path.exists() or full_path.is_dir():
259
+ logger.debug(
260
+ "Skipping non-existent or directory path", path=path
261
+ ) # pragma: no cover
262
+ processed.add(path) # pragma: no cover
263
+ continue # pragma: no cover
254
264
 
255
265
  logger.debug("Processing new file", path=path)
256
266
  entity, checksum = await self.sync_service.sync_file(path, new=True)
@@ -281,8 +291,8 @@ class WatchService:
281
291
  if path not in processed:
282
292
  # Skip directories - only process files
283
293
  full_path = directory / path
284
- if full_path.is_dir():
285
- logger.debug("Skipping directory", path=path)
294
+ if not full_path.exists() or full_path.is_dir():
295
+ logger.debug("Skipping non-existent or directory path", path=path)
286
296
  processed.add(path)
287
297
  continue
288
298
 
@@ -341,4 +351,4 @@ class WatchService:
341
351
  duration_ms=duration_ms,
342
352
  )
343
353
 
344
- await self.write_status()
354
+ await self.write_status()