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.

Files changed (76) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/migrations.py +4 -9
  3. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +106 -0
  4. basic_memory/api/app.py +9 -6
  5. basic_memory/api/routers/__init__.py +2 -1
  6. basic_memory/api/routers/knowledge_router.py +30 -4
  7. basic_memory/api/routers/memory_router.py +3 -2
  8. basic_memory/api/routers/project_info_router.py +274 -0
  9. basic_memory/api/routers/search_router.py +22 -4
  10. basic_memory/cli/app.py +54 -3
  11. basic_memory/cli/commands/__init__.py +15 -2
  12. basic_memory/cli/commands/db.py +9 -13
  13. basic_memory/cli/commands/import_chatgpt.py +31 -36
  14. basic_memory/cli/commands/import_claude_conversations.py +32 -35
  15. basic_memory/cli/commands/import_claude_projects.py +34 -37
  16. basic_memory/cli/commands/import_memory_json.py +26 -28
  17. basic_memory/cli/commands/mcp.py +7 -1
  18. basic_memory/cli/commands/project.py +119 -0
  19. basic_memory/cli/commands/project_info.py +167 -0
  20. basic_memory/cli/commands/status.py +7 -9
  21. basic_memory/cli/commands/sync.py +54 -9
  22. basic_memory/cli/commands/{tools.py → tool.py} +92 -19
  23. basic_memory/cli/main.py +40 -1
  24. basic_memory/config.py +157 -10
  25. basic_memory/db.py +19 -4
  26. basic_memory/deps.py +10 -3
  27. basic_memory/file_utils.py +34 -18
  28. basic_memory/markdown/markdown_processor.py +1 -1
  29. basic_memory/markdown/utils.py +5 -0
  30. basic_memory/mcp/main.py +1 -2
  31. basic_memory/mcp/prompts/__init__.py +6 -2
  32. basic_memory/mcp/prompts/ai_assistant_guide.py +9 -10
  33. basic_memory/mcp/prompts/continue_conversation.py +65 -126
  34. basic_memory/mcp/prompts/recent_activity.py +55 -13
  35. basic_memory/mcp/prompts/search.py +72 -17
  36. basic_memory/mcp/prompts/utils.py +139 -82
  37. basic_memory/mcp/server.py +1 -1
  38. basic_memory/mcp/tools/__init__.py +11 -22
  39. basic_memory/mcp/tools/build_context.py +85 -0
  40. basic_memory/mcp/tools/canvas.py +17 -19
  41. basic_memory/mcp/tools/delete_note.py +28 -0
  42. basic_memory/mcp/tools/project_info.py +51 -0
  43. basic_memory/mcp/tools/{resource.py → read_content.py} +42 -5
  44. basic_memory/mcp/tools/read_note.py +190 -0
  45. basic_memory/mcp/tools/recent_activity.py +100 -0
  46. basic_memory/mcp/tools/search.py +56 -17
  47. basic_memory/mcp/tools/utils.py +245 -17
  48. basic_memory/mcp/tools/write_note.py +124 -0
  49. basic_memory/models/search.py +2 -1
  50. basic_memory/repository/entity_repository.py +3 -2
  51. basic_memory/repository/project_info_repository.py +9 -0
  52. basic_memory/repository/repository.py +23 -6
  53. basic_memory/repository/search_repository.py +33 -10
  54. basic_memory/schemas/__init__.py +12 -0
  55. basic_memory/schemas/memory.py +3 -2
  56. basic_memory/schemas/project_info.py +96 -0
  57. basic_memory/schemas/search.py +27 -32
  58. basic_memory/services/context_service.py +3 -3
  59. basic_memory/services/entity_service.py +8 -2
  60. basic_memory/services/file_service.py +107 -57
  61. basic_memory/services/link_resolver.py +5 -45
  62. basic_memory/services/search_service.py +45 -16
  63. basic_memory/sync/sync_service.py +274 -39
  64. basic_memory/sync/watch_service.py +174 -34
  65. basic_memory/utils.py +40 -40
  66. basic_memory-0.10.0.dist-info/METADATA +386 -0
  67. basic_memory-0.10.0.dist-info/RECORD +99 -0
  68. basic_memory/mcp/prompts/json_canvas_spec.py +0 -25
  69. basic_memory/mcp/tools/knowledge.py +0 -68
  70. basic_memory/mcp/tools/memory.py +0 -177
  71. basic_memory/mcp/tools/notes.py +0 -201
  72. basic_memory-0.8.0.dist-info/METADATA +0 -379
  73. basic_memory-0.8.0.dist-info/RECORD +0 -91
  74. {basic_memory-0.8.0.dist-info → basic_memory-0.10.0.dist-info}/WHEEL +0 -0
  75. {basic_memory-0.8.0.dist-info → basic_memory-0.10.0.dist-info}/entry_points.txt +0 -0
  76. {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
- # Load project config
66
- config = ProjectConfig()
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=config.home,
218
+ home_dir=user_home, # Use user home for logs
72
219
  log_level=config.log_level,
73
- log_file=".basic-memory/basic-memory.log",
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
- assert _engine is not None # for type checker
90
- assert _session_maker is not None # for type checker
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
- assert _engine is not None # for type checker
125
- assert _session_maker is not None # for type checker
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
 
@@ -2,11 +2,13 @@
2
2
 
3
3
  import hashlib
4
4
  from pathlib import Path
5
- from typing import Dict, Any, Union
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: Path) -> None:
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
- path.mkdir(parents=True, exist_ok=True)
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(f"Failed to create directory: {path}: {e}")
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: Path, content: str) -> None:
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
- temp_path = path.with_suffix(".tmp")
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(path)
83
- logger.debug(f"wrote file: {path}")
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(f"Failed to write file: {path}: {e}")
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: Path, updates: Dict[str, Any]) -> str:
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 = path.read_text()
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
- await write_file_atomic(path, final_content)
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(f"Failed to update frontmatter in {path}: {e}")
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")
@@ -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__ = ["ai_assistant_guide", "continue_conversation", "json_canvas_spec", "recent_activity", "search"]
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="ai_assistant_guide",
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
- with logfire.span("Getting Basic Memory guide"): # pyright: ignore
24
- logger.info("Loading AI assistant guide resource")
25
- guide_doc = Path(__file__).parent.parent.parent.parent.parent / "data/ai_assistant_guide.md"
26
- content = guide_doc.read_text()
27
- logger.info(f"Loaded AI assistant guide ({len(content)} chars)")
28
- return content
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