basic-memory 0.8.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 +1 -1
- basic_memory/alembic/migrations.py +4 -9
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +106 -0
- basic_memory/api/app.py +9 -6
- basic_memory/api/routers/__init__.py +2 -1
- basic_memory/api/routers/knowledge_router.py +30 -4
- basic_memory/api/routers/memory_router.py +3 -2
- basic_memory/api/routers/project_info_router.py +274 -0
- basic_memory/api/routers/search_router.py +22 -4
- basic_memory/cli/app.py +54 -3
- basic_memory/cli/commands/__init__.py +15 -2
- basic_memory/cli/commands/db.py +9 -13
- basic_memory/cli/commands/import_chatgpt.py +31 -36
- basic_memory/cli/commands/import_claude_conversations.py +32 -35
- basic_memory/cli/commands/import_claude_projects.py +34 -37
- basic_memory/cli/commands/import_memory_json.py +26 -28
- basic_memory/cli/commands/mcp.py +7 -1
- basic_memory/cli/commands/project.py +119 -0
- basic_memory/cli/commands/project_info.py +167 -0
- basic_memory/cli/commands/status.py +7 -9
- basic_memory/cli/commands/sync.py +54 -9
- basic_memory/cli/commands/{tools.py → tool.py} +92 -19
- basic_memory/cli/main.py +40 -1
- basic_memory/config.py +157 -10
- basic_memory/db.py +19 -4
- basic_memory/deps.py +10 -3
- basic_memory/file_utils.py +34 -18
- basic_memory/markdown/markdown_processor.py +1 -1
- basic_memory/markdown/utils.py +5 -0
- basic_memory/mcp/main.py +1 -2
- basic_memory/mcp/prompts/__init__.py +6 -2
- basic_memory/mcp/prompts/ai_assistant_guide.py +9 -10
- basic_memory/mcp/prompts/continue_conversation.py +65 -126
- basic_memory/mcp/prompts/recent_activity.py +55 -13
- basic_memory/mcp/prompts/search.py +72 -17
- basic_memory/mcp/prompts/utils.py +139 -82
- basic_memory/mcp/server.py +1 -1
- basic_memory/mcp/tools/__init__.py +11 -22
- basic_memory/mcp/tools/build_context.py +85 -0
- basic_memory/mcp/tools/canvas.py +17 -19
- basic_memory/mcp/tools/delete_note.py +28 -0
- basic_memory/mcp/tools/project_info.py +51 -0
- basic_memory/mcp/tools/{resource.py → read_content.py} +42 -5
- basic_memory/mcp/tools/read_note.py +190 -0
- basic_memory/mcp/tools/recent_activity.py +100 -0
- basic_memory/mcp/tools/search.py +56 -17
- basic_memory/mcp/tools/utils.py +245 -17
- basic_memory/mcp/tools/write_note.py +124 -0
- basic_memory/models/search.py +2 -1
- basic_memory/repository/entity_repository.py +3 -2
- basic_memory/repository/project_info_repository.py +9 -0
- basic_memory/repository/repository.py +23 -6
- basic_memory/repository/search_repository.py +33 -10
- basic_memory/schemas/__init__.py +12 -0
- basic_memory/schemas/memory.py +3 -2
- basic_memory/schemas/project_info.py +96 -0
- basic_memory/schemas/search.py +27 -32
- basic_memory/services/context_service.py +3 -3
- basic_memory/services/entity_service.py +8 -2
- basic_memory/services/file_service.py +107 -57
- basic_memory/services/link_resolver.py +5 -45
- basic_memory/services/search_service.py +45 -16
- basic_memory/sync/sync_service.py +274 -39
- basic_memory/sync/watch_service.py +174 -34
- basic_memory/utils.py +40 -40
- basic_memory-0.10.0.dist-info/METADATA +386 -0
- basic_memory-0.10.0.dist-info/RECORD +99 -0
- basic_memory/mcp/prompts/json_canvas_spec.py +0 -25
- basic_memory/mcp/tools/knowledge.py +0 -68
- basic_memory/mcp/tools/memory.py +0 -177
- basic_memory/mcp/tools/notes.py +0 -201
- basic_memory-0.8.0.dist-info/METADATA +0 -379
- basic_memory-0.8.0.dist-info/RECORD +0 -91
- {basic_memory-0.8.0.dist-info → basic_memory-0.10.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.8.0.dist-info → basic_memory-0.10.0.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.8.0.dist-info → basic_memory-0.10.0.dist-info}/licenses/LICENSE +0 -0
basic_memory/config.py
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
"""Configuration management for basic-memory."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
3
5
|
from pathlib import Path
|
|
4
|
-
from typing import Literal
|
|
6
|
+
from typing import Any, Dict, Literal, Optional
|
|
5
7
|
|
|
8
|
+
import basic_memory
|
|
9
|
+
from basic_memory.utils import setup_logging
|
|
6
10
|
from loguru import logger
|
|
7
11
|
from pydantic import Field, field_validator
|
|
8
12
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
9
13
|
|
|
10
|
-
import basic_memory
|
|
11
|
-
from basic_memory.utils import setup_logging
|
|
12
|
-
|
|
13
14
|
DATABASE_NAME = "memory.db"
|
|
14
15
|
DATA_DIR_NAME = ".basic-memory"
|
|
16
|
+
CONFIG_FILE_NAME = "config.json"
|
|
15
17
|
|
|
16
18
|
Environment = Literal["test", "dev", "user"]
|
|
17
19
|
|
|
@@ -62,15 +64,160 @@ class ProjectConfig(BaseSettings):
|
|
|
62
64
|
return v
|
|
63
65
|
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
class BasicMemoryConfig(BaseSettings):
|
|
68
|
+
"""Pydantic model for Basic Memory global configuration."""
|
|
69
|
+
|
|
70
|
+
projects: Dict[str, str] = Field(
|
|
71
|
+
default_factory=lambda: {"main": str(Path.home() / "basic-memory")},
|
|
72
|
+
description="Mapping of project names to their filesystem paths",
|
|
73
|
+
)
|
|
74
|
+
default_project: str = Field(
|
|
75
|
+
default="main",
|
|
76
|
+
description="Name of the default project to use",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
model_config = SettingsConfigDict(
|
|
80
|
+
env_prefix="BASIC_MEMORY_",
|
|
81
|
+
extra="ignore",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def model_post_init(self, __context: Any) -> None:
|
|
85
|
+
"""Ensure configuration is valid after initialization."""
|
|
86
|
+
# Ensure main project exists
|
|
87
|
+
if "main" not in self.projects:
|
|
88
|
+
self.projects["main"] = str(Path.home() / "basic-memory")
|
|
89
|
+
|
|
90
|
+
# Ensure default project is valid
|
|
91
|
+
if self.default_project not in self.projects:
|
|
92
|
+
self.default_project = "main"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ConfigManager:
|
|
96
|
+
"""Manages Basic Memory configuration."""
|
|
97
|
+
|
|
98
|
+
def __init__(self) -> None:
|
|
99
|
+
"""Initialize the configuration manager."""
|
|
100
|
+
self.config_dir = Path.home() / DATA_DIR_NAME
|
|
101
|
+
self.config_file = self.config_dir / CONFIG_FILE_NAME
|
|
102
|
+
|
|
103
|
+
# Ensure config directory exists
|
|
104
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
|
|
106
|
+
# Load or create configuration
|
|
107
|
+
self.config = self.load_config()
|
|
108
|
+
|
|
109
|
+
def load_config(self) -> BasicMemoryConfig:
|
|
110
|
+
"""Load configuration from file or create default."""
|
|
111
|
+
if self.config_file.exists():
|
|
112
|
+
try:
|
|
113
|
+
data = json.loads(self.config_file.read_text(encoding="utf-8"))
|
|
114
|
+
return BasicMemoryConfig(**data)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.error(f"Failed to load config: {e}")
|
|
117
|
+
config = BasicMemoryConfig()
|
|
118
|
+
self.save_config(config)
|
|
119
|
+
return config
|
|
120
|
+
else:
|
|
121
|
+
config = BasicMemoryConfig()
|
|
122
|
+
self.save_config(config)
|
|
123
|
+
return config
|
|
124
|
+
|
|
125
|
+
def save_config(self, config: BasicMemoryConfig) -> None:
|
|
126
|
+
"""Save configuration to file."""
|
|
127
|
+
try:
|
|
128
|
+
self.config_file.write_text(json.dumps(config.model_dump(), indent=2))
|
|
129
|
+
except Exception as e: # pragma: no cover
|
|
130
|
+
logger.error(f"Failed to save config: {e}")
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def projects(self) -> Dict[str, str]:
|
|
134
|
+
"""Get all configured projects."""
|
|
135
|
+
return self.config.projects.copy()
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def default_project(self) -> str:
|
|
139
|
+
"""Get the default project name."""
|
|
140
|
+
return self.config.default_project
|
|
141
|
+
|
|
142
|
+
def get_project_path(self, project_name: Optional[str] = None) -> Path:
|
|
143
|
+
"""Get the path for a specific project or the default project."""
|
|
144
|
+
name = project_name or self.config.default_project
|
|
145
|
+
|
|
146
|
+
# Check if specified in environment variable
|
|
147
|
+
if not project_name and "BASIC_MEMORY_PROJECT" in os.environ:
|
|
148
|
+
name = os.environ["BASIC_MEMORY_PROJECT"]
|
|
149
|
+
|
|
150
|
+
if name not in self.config.projects:
|
|
151
|
+
raise ValueError(f"Project '{name}' not found in configuration")
|
|
152
|
+
|
|
153
|
+
return Path(self.config.projects[name])
|
|
154
|
+
|
|
155
|
+
def add_project(self, name: str, path: str) -> None:
|
|
156
|
+
"""Add a new project to the configuration."""
|
|
157
|
+
if name in self.config.projects:
|
|
158
|
+
raise ValueError(f"Project '{name}' already exists")
|
|
159
|
+
|
|
160
|
+
# Ensure the path exists
|
|
161
|
+
project_path = Path(path)
|
|
162
|
+
project_path.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
|
|
164
|
+
self.config.projects[name] = str(project_path)
|
|
165
|
+
self.save_config(self.config)
|
|
166
|
+
|
|
167
|
+
def remove_project(self, name: str) -> None:
|
|
168
|
+
"""Remove a project from the configuration."""
|
|
169
|
+
if name not in self.config.projects:
|
|
170
|
+
raise ValueError(f"Project '{name}' not found")
|
|
171
|
+
|
|
172
|
+
if name == self.config.default_project:
|
|
173
|
+
raise ValueError(f"Cannot remove the default project '{name}'")
|
|
174
|
+
|
|
175
|
+
del self.config.projects[name]
|
|
176
|
+
self.save_config(self.config)
|
|
177
|
+
|
|
178
|
+
def set_default_project(self, name: str) -> None:
|
|
179
|
+
"""Set the default project."""
|
|
180
|
+
if name not in self.config.projects: # pragma: no cover
|
|
181
|
+
raise ValueError(f"Project '{name}' not found")
|
|
182
|
+
|
|
183
|
+
self.config.default_project = name
|
|
184
|
+
self.save_config(self.config)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
|
|
188
|
+
"""Get a project configuration for the specified project."""
|
|
189
|
+
config_manager = ConfigManager()
|
|
190
|
+
|
|
191
|
+
# Get project name from environment variable or use provided name or default
|
|
192
|
+
actual_project_name = os.environ.get(
|
|
193
|
+
"BASIC_MEMORY_PROJECT", project_name or config_manager.default_project
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
project_path = config_manager.get_project_path(actual_project_name)
|
|
198
|
+
return ProjectConfig(home=project_path, project=actual_project_name)
|
|
199
|
+
except ValueError: # pragma: no cover
|
|
200
|
+
logger.warning(f"Project '{actual_project_name}' not found, using default")
|
|
201
|
+
project_path = config_manager.get_project_path(config_manager.default_project)
|
|
202
|
+
return ProjectConfig(home=project_path, project=config_manager.default_project)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# Create config manager
|
|
206
|
+
config_manager = ConfigManager()
|
|
207
|
+
|
|
208
|
+
# Load project config for current context
|
|
209
|
+
config = get_project_config()
|
|
210
|
+
|
|
211
|
+
# setup logging to a single log file in user home directory
|
|
212
|
+
user_home = Path.home()
|
|
213
|
+
log_dir = user_home / DATA_DIR_NAME
|
|
214
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
67
215
|
|
|
68
|
-
# setup logging
|
|
69
216
|
setup_logging(
|
|
70
217
|
env=config.env,
|
|
71
|
-
home_dir=
|
|
218
|
+
home_dir=user_home, # Use user home for logs
|
|
72
219
|
log_level=config.log_level,
|
|
73
|
-
log_file="
|
|
220
|
+
log_file=f"{DATA_DIR_NAME}/basic-memory.log",
|
|
74
221
|
console=False,
|
|
75
222
|
)
|
|
76
|
-
logger.info(f"Starting Basic Memory {basic_memory.__version__}")
|
|
223
|
+
logger.info(f"Starting Basic Memory {basic_memory.__version__} (Project: {config.project})")
|
basic_memory/db.py
CHANGED
|
@@ -86,8 +86,16 @@ async def get_or_create_db(
|
|
|
86
86
|
_engine = create_async_engine(db_url, connect_args={"check_same_thread": False})
|
|
87
87
|
_session_maker = async_sessionmaker(_engine, expire_on_commit=False)
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
# These checks should never fail since we just created the engine and session maker
|
|
90
|
+
# if they were None, but we'll check anyway for the type checker
|
|
91
|
+
if _engine is None:
|
|
92
|
+
logger.error("Failed to create database engine", db_path=str(db_path))
|
|
93
|
+
raise RuntimeError("Database engine initialization failed")
|
|
94
|
+
|
|
95
|
+
if _session_maker is None:
|
|
96
|
+
logger.error("Failed to create session maker", db_path=str(db_path))
|
|
97
|
+
raise RuntimeError("Session maker initialization failed")
|
|
98
|
+
|
|
91
99
|
return _engine, _session_maker
|
|
92
100
|
|
|
93
101
|
|
|
@@ -121,8 +129,15 @@ async def engine_session_factory(
|
|
|
121
129
|
try:
|
|
122
130
|
_session_maker = async_sessionmaker(_engine, expire_on_commit=False)
|
|
123
131
|
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
# Verify that engine and session maker are initialized
|
|
133
|
+
if _engine is None: # pragma: no cover
|
|
134
|
+
logger.error("Database engine is None in engine_session_factory")
|
|
135
|
+
raise RuntimeError("Database engine initialization failed")
|
|
136
|
+
|
|
137
|
+
if _session_maker is None: # pragma: no cover
|
|
138
|
+
logger.error("Session maker is None in engine_session_factory")
|
|
139
|
+
raise RuntimeError("Session maker initialization failed")
|
|
140
|
+
|
|
126
141
|
yield _engine, _session_maker
|
|
127
142
|
finally:
|
|
128
143
|
if _engine:
|
basic_memory/deps.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Annotated
|
|
4
4
|
|
|
5
|
-
import logfire
|
|
6
5
|
from fastapi import Depends
|
|
7
6
|
from sqlalchemy.ext.asyncio import (
|
|
8
7
|
AsyncSession,
|
|
@@ -16,6 +15,7 @@ from basic_memory.markdown import EntityParser
|
|
|
16
15
|
from basic_memory.markdown.markdown_processor import MarkdownProcessor
|
|
17
16
|
from basic_memory.repository.entity_repository import EntityRepository
|
|
18
17
|
from basic_memory.repository.observation_repository import ObservationRepository
|
|
18
|
+
from basic_memory.repository.project_info_repository import ProjectInfoRepository
|
|
19
19
|
from basic_memory.repository.relation_repository import RelationRepository
|
|
20
20
|
from basic_memory.repository.search_repository import SearchRepository
|
|
21
21
|
from basic_memory.services import (
|
|
@@ -45,8 +45,6 @@ async def get_engine_factory(
|
|
|
45
45
|
) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: # pragma: no cover
|
|
46
46
|
"""Get engine and session maker."""
|
|
47
47
|
engine, session_maker = await db.get_or_create_db(project_config.database_path)
|
|
48
|
-
if project_config.env != "test":
|
|
49
|
-
logfire.instrument_sqlalchemy(engine=engine)
|
|
50
48
|
return engine, session_maker
|
|
51
49
|
|
|
52
50
|
|
|
@@ -107,6 +105,15 @@ async def get_search_repository(
|
|
|
107
105
|
SearchRepositoryDep = Annotated[SearchRepository, Depends(get_search_repository)]
|
|
108
106
|
|
|
109
107
|
|
|
108
|
+
def get_project_info_repository(
|
|
109
|
+
session_maker: SessionMakerDep,
|
|
110
|
+
):
|
|
111
|
+
"""Dependency for StatsRepository."""
|
|
112
|
+
return ProjectInfoRepository(session_maker)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
ProjectInfoRepositoryDep = Annotated[ProjectInfoRepository, Depends(get_project_info_repository)]
|
|
116
|
+
|
|
110
117
|
## services
|
|
111
118
|
|
|
112
119
|
|
basic_memory/file_utils.py
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Any, Dict, Union
|
|
6
6
|
|
|
7
7
|
import yaml
|
|
8
8
|
from loguru import logger
|
|
9
9
|
|
|
10
|
+
from basic_memory.utils import FilePath
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
class FileError(Exception):
|
|
12
14
|
"""Base exception for file operations."""
|
|
@@ -48,42 +50,47 @@ async def compute_checksum(content: Union[str, bytes]) -> str:
|
|
|
48
50
|
raise FileError(f"Failed to compute checksum: {e}")
|
|
49
51
|
|
|
50
52
|
|
|
51
|
-
async def ensure_directory(path:
|
|
53
|
+
async def ensure_directory(path: FilePath) -> None:
|
|
52
54
|
"""
|
|
53
55
|
Ensure directory exists, creating if necessary.
|
|
54
56
|
|
|
55
57
|
Args:
|
|
56
|
-
path: Directory path to ensure
|
|
58
|
+
path: Directory path to ensure (Path or string)
|
|
57
59
|
|
|
58
60
|
Raises:
|
|
59
61
|
FileWriteError: If directory creation fails
|
|
60
62
|
"""
|
|
61
63
|
try:
|
|
62
|
-
|
|
64
|
+
# Convert string to Path if needed
|
|
65
|
+
path_obj = Path(path) if isinstance(path, str) else path
|
|
66
|
+
path_obj.mkdir(parents=True, exist_ok=True)
|
|
63
67
|
except Exception as e: # pragma: no cover
|
|
64
|
-
logger.error(
|
|
68
|
+
logger.error("Failed to create directory", path=str(path), error=str(e))
|
|
65
69
|
raise FileWriteError(f"Failed to create directory {path}: {e}")
|
|
66
70
|
|
|
67
71
|
|
|
68
|
-
async def write_file_atomic(path:
|
|
72
|
+
async def write_file_atomic(path: FilePath, content: str) -> None:
|
|
69
73
|
"""
|
|
70
74
|
Write file with atomic operation using temporary file.
|
|
71
75
|
|
|
72
76
|
Args:
|
|
73
|
-
path: Target file path
|
|
77
|
+
path: Target file path (Path or string)
|
|
74
78
|
content: Content to write
|
|
75
79
|
|
|
76
80
|
Raises:
|
|
77
81
|
FileWriteError: If write operation fails
|
|
78
82
|
"""
|
|
79
|
-
|
|
83
|
+
# Convert string to Path if needed
|
|
84
|
+
path_obj = Path(path) if isinstance(path, str) else path
|
|
85
|
+
temp_path = path_obj.with_suffix(".tmp")
|
|
86
|
+
|
|
80
87
|
try:
|
|
81
|
-
temp_path.write_text(content)
|
|
82
|
-
temp_path.replace(
|
|
83
|
-
logger.debug(
|
|
88
|
+
temp_path.write_text(content, encoding="utf-8")
|
|
89
|
+
temp_path.replace(path_obj)
|
|
90
|
+
logger.debug("Wrote file atomically", path=str(path_obj), content_length=len(content))
|
|
84
91
|
except Exception as e: # pragma: no cover
|
|
85
92
|
temp_path.unlink(missing_ok=True)
|
|
86
|
-
logger.error(
|
|
93
|
+
logger.error("Failed to write file", path=str(path_obj), error=str(e))
|
|
87
94
|
raise FileWriteError(f"Failed to write file {path}: {e}")
|
|
88
95
|
|
|
89
96
|
|
|
@@ -173,7 +180,7 @@ def remove_frontmatter(content: str) -> str:
|
|
|
173
180
|
return parts[2].strip()
|
|
174
181
|
|
|
175
182
|
|
|
176
|
-
async def update_frontmatter(path:
|
|
183
|
+
async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
|
|
177
184
|
"""Update frontmatter fields in a file while preserving all content.
|
|
178
185
|
|
|
179
186
|
Only modifies the frontmatter section, leaving all content untouched.
|
|
@@ -181,7 +188,7 @@ async def update_frontmatter(path: Path, updates: Dict[str, Any]) -> str:
|
|
|
181
188
|
Returns checksum of updated file.
|
|
182
189
|
|
|
183
190
|
Args:
|
|
184
|
-
path: Path to markdown file
|
|
191
|
+
path: Path to markdown file (Path or string)
|
|
185
192
|
updates: Dict of frontmatter fields to update
|
|
186
193
|
|
|
187
194
|
Returns:
|
|
@@ -192,8 +199,11 @@ async def update_frontmatter(path: Path, updates: Dict[str, Any]) -> str:
|
|
|
192
199
|
ParseError: If frontmatter parsing fails
|
|
193
200
|
"""
|
|
194
201
|
try:
|
|
202
|
+
# Convert string to Path if needed
|
|
203
|
+
path_obj = Path(path) if isinstance(path, str) else path
|
|
204
|
+
|
|
195
205
|
# Read current content
|
|
196
|
-
content =
|
|
206
|
+
content = path_obj.read_text(encoding="utf-8")
|
|
197
207
|
|
|
198
208
|
# Parse current frontmatter
|
|
199
209
|
current_fm = {}
|
|
@@ -205,12 +215,18 @@ async def update_frontmatter(path: Path, updates: Dict[str, Any]) -> str:
|
|
|
205
215
|
new_fm = {**current_fm, **updates}
|
|
206
216
|
|
|
207
217
|
# Write new file with updated frontmatter
|
|
208
|
-
yaml_fm = yaml.dump(new_fm, sort_keys=False)
|
|
218
|
+
yaml_fm = yaml.dump(new_fm, sort_keys=False, allow_unicode=True)
|
|
209
219
|
final_content = f"---\n{yaml_fm}---\n\n{content.strip()}"
|
|
210
220
|
|
|
211
|
-
|
|
221
|
+
logger.debug("Updating frontmatter", path=str(path_obj), update_keys=list(updates.keys()))
|
|
222
|
+
|
|
223
|
+
await write_file_atomic(path_obj, final_content)
|
|
212
224
|
return await compute_checksum(final_content)
|
|
213
225
|
|
|
214
226
|
except Exception as e: # pragma: no cover
|
|
215
|
-
logger.error(
|
|
227
|
+
logger.error(
|
|
228
|
+
"Failed to update frontmatter",
|
|
229
|
+
path=str(path) if isinstance(path, (str, Path)) else "<unknown>",
|
|
230
|
+
error=str(e),
|
|
231
|
+
)
|
|
216
232
|
raise FileError(f"Failed to update frontmatter: {e}")
|
|
@@ -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")
|
basic_memory/markdown/utils.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import Optional, Any
|
|
|
5
5
|
|
|
6
6
|
from frontmatter import Post
|
|
7
7
|
|
|
8
|
+
from basic_memory.file_utils import has_frontmatter, remove_frontmatter
|
|
8
9
|
from basic_memory.markdown import EntityMarkdown
|
|
9
10
|
from basic_memory.models import Entity, Observation as ObservationModel
|
|
10
11
|
from basic_memory.utils import generate_permalink
|
|
@@ -78,6 +79,10 @@ async def schema_to_markdown(schema: Any) -> Post:
|
|
|
78
79
|
content = schema.content or ""
|
|
79
80
|
frontmatter_metadata = dict(schema.entity_metadata or {})
|
|
80
81
|
|
|
82
|
+
# if the content contains frontmatter, remove it and merge
|
|
83
|
+
if has_frontmatter(content):
|
|
84
|
+
content = remove_frontmatter(content)
|
|
85
|
+
|
|
81
86
|
# Remove special fields for ordered frontmatter
|
|
82
87
|
for field in ["type", "title", "permalink"]:
|
|
83
88
|
frontmatter_metadata.pop(field, None)
|
basic_memory/mcp/main.py
CHANGED
|
@@ -17,9 +17,8 @@ import basic_memory.mcp.tools # noqa: F401 # pragma: no cover
|
|
|
17
17
|
import basic_memory.mcp.prompts # noqa: F401 # pragma: no cover
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
|
|
21
20
|
if __name__ == "__main__": # pragma: no cover
|
|
22
21
|
home_dir = config.home
|
|
23
22
|
logger.info("Starting Basic Memory MCP server")
|
|
24
23
|
logger.info(f"Home directory: {home_dir}")
|
|
25
|
-
mcp.run()
|
|
24
|
+
mcp.run()
|
|
@@ -10,6 +10,10 @@ from basic_memory.mcp.prompts import continue_conversation
|
|
|
10
10
|
from basic_memory.mcp.prompts import recent_activity
|
|
11
11
|
from basic_memory.mcp.prompts import search
|
|
12
12
|
from basic_memory.mcp.prompts import ai_assistant_guide
|
|
13
|
-
from basic_memory.mcp.prompts import json_canvas_spec
|
|
14
13
|
|
|
15
|
-
__all__ = [
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ai_assistant_guide",
|
|
16
|
+
"continue_conversation",
|
|
17
|
+
"recent_activity",
|
|
18
|
+
"search",
|
|
19
|
+
]
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
import logfire
|
|
4
|
-
from loguru import logger
|
|
5
|
-
|
|
6
3
|
from basic_memory.mcp.server import mcp
|
|
4
|
+
from loguru import logger
|
|
7
5
|
|
|
8
6
|
|
|
9
7
|
@mcp.resource(
|
|
10
8
|
uri="memory://ai_assistant_guide",
|
|
11
|
-
name="
|
|
9
|
+
name="ai assistant guide",
|
|
12
10
|
description="Give an AI assistant guidance on how to use Basic Memory tools effectively",
|
|
13
11
|
)
|
|
14
12
|
def ai_assistant_guide() -> str:
|
|
@@ -20,9 +18,10 @@ def ai_assistant_guide() -> str:
|
|
|
20
18
|
Returns:
|
|
21
19
|
A focused guide on Basic Memory usage.
|
|
22
20
|
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
logger.info("Loading AI assistant guide resource")
|
|
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")
|
|
26
|
+
logger.info(f"Loaded AI assistant guide ({len(content)} chars)")
|
|
27
|
+
return content
|