basic-memory 0.7.0__py3-none-any.whl → 0.16.1__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 (150) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +27 -3
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  6. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  7. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -0
  8. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  9. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  10. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  11. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +100 -0
  12. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  13. basic_memory/api/app.py +64 -18
  14. basic_memory/api/routers/__init__.py +4 -1
  15. basic_memory/api/routers/directory_router.py +84 -0
  16. basic_memory/api/routers/importer_router.py +152 -0
  17. basic_memory/api/routers/knowledge_router.py +166 -21
  18. basic_memory/api/routers/management_router.py +80 -0
  19. basic_memory/api/routers/memory_router.py +9 -64
  20. basic_memory/api/routers/project_router.py +406 -0
  21. basic_memory/api/routers/prompt_router.py +260 -0
  22. basic_memory/api/routers/resource_router.py +119 -4
  23. basic_memory/api/routers/search_router.py +5 -5
  24. basic_memory/api/routers/utils.py +130 -0
  25. basic_memory/api/template_loader.py +292 -0
  26. basic_memory/cli/app.py +43 -9
  27. basic_memory/cli/auth.py +277 -0
  28. basic_memory/cli/commands/__init__.py +13 -2
  29. basic_memory/cli/commands/cloud/__init__.py +6 -0
  30. basic_memory/cli/commands/cloud/api_client.py +112 -0
  31. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  32. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  33. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  34. basic_memory/cli/commands/cloud/rclone_commands.py +301 -0
  35. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  36. basic_memory/cli/commands/cloud/rclone_installer.py +249 -0
  37. basic_memory/cli/commands/cloud/upload.py +233 -0
  38. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  39. basic_memory/cli/commands/command_utils.py +51 -0
  40. basic_memory/cli/commands/db.py +28 -12
  41. basic_memory/cli/commands/import_chatgpt.py +40 -220
  42. basic_memory/cli/commands/import_claude_conversations.py +41 -168
  43. basic_memory/cli/commands/import_claude_projects.py +46 -157
  44. basic_memory/cli/commands/import_memory_json.py +48 -108
  45. basic_memory/cli/commands/mcp.py +84 -10
  46. basic_memory/cli/commands/project.py +876 -0
  47. basic_memory/cli/commands/status.py +50 -33
  48. basic_memory/cli/commands/tool.py +341 -0
  49. basic_memory/cli/main.py +8 -7
  50. basic_memory/config.py +477 -23
  51. basic_memory/db.py +168 -17
  52. basic_memory/deps.py +251 -25
  53. basic_memory/file_utils.py +113 -58
  54. basic_memory/ignore_utils.py +297 -0
  55. basic_memory/importers/__init__.py +27 -0
  56. basic_memory/importers/base.py +79 -0
  57. basic_memory/importers/chatgpt_importer.py +232 -0
  58. basic_memory/importers/claude_conversations_importer.py +177 -0
  59. basic_memory/importers/claude_projects_importer.py +148 -0
  60. basic_memory/importers/memory_json_importer.py +108 -0
  61. basic_memory/importers/utils.py +58 -0
  62. basic_memory/markdown/entity_parser.py +143 -23
  63. basic_memory/markdown/markdown_processor.py +3 -3
  64. basic_memory/markdown/plugins.py +39 -21
  65. basic_memory/markdown/schemas.py +1 -1
  66. basic_memory/markdown/utils.py +28 -13
  67. basic_memory/mcp/async_client.py +134 -4
  68. basic_memory/mcp/project_context.py +141 -0
  69. basic_memory/mcp/prompts/__init__.py +19 -0
  70. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  71. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  72. basic_memory/mcp/prompts/recent_activity.py +188 -0
  73. basic_memory/mcp/prompts/search.py +57 -0
  74. basic_memory/mcp/prompts/utils.py +162 -0
  75. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  76. basic_memory/mcp/resources/project_info.py +71 -0
  77. basic_memory/mcp/server.py +7 -13
  78. basic_memory/mcp/tools/__init__.py +33 -21
  79. basic_memory/mcp/tools/build_context.py +120 -0
  80. basic_memory/mcp/tools/canvas.py +130 -0
  81. basic_memory/mcp/tools/chatgpt_tools.py +187 -0
  82. basic_memory/mcp/tools/delete_note.py +225 -0
  83. basic_memory/mcp/tools/edit_note.py +320 -0
  84. basic_memory/mcp/tools/list_directory.py +167 -0
  85. basic_memory/mcp/tools/move_note.py +545 -0
  86. basic_memory/mcp/tools/project_management.py +200 -0
  87. basic_memory/mcp/tools/read_content.py +271 -0
  88. basic_memory/mcp/tools/read_note.py +255 -0
  89. basic_memory/mcp/tools/recent_activity.py +534 -0
  90. basic_memory/mcp/tools/search.py +369 -23
  91. basic_memory/mcp/tools/utils.py +374 -16
  92. basic_memory/mcp/tools/view_note.py +77 -0
  93. basic_memory/mcp/tools/write_note.py +207 -0
  94. basic_memory/models/__init__.py +3 -2
  95. basic_memory/models/knowledge.py +67 -15
  96. basic_memory/models/project.py +87 -0
  97. basic_memory/models/search.py +10 -6
  98. basic_memory/repository/__init__.py +2 -0
  99. basic_memory/repository/entity_repository.py +229 -7
  100. basic_memory/repository/observation_repository.py +35 -3
  101. basic_memory/repository/project_info_repository.py +10 -0
  102. basic_memory/repository/project_repository.py +103 -0
  103. basic_memory/repository/relation_repository.py +21 -2
  104. basic_memory/repository/repository.py +147 -29
  105. basic_memory/repository/search_repository.py +411 -62
  106. basic_memory/schemas/__init__.py +22 -9
  107. basic_memory/schemas/base.py +97 -8
  108. basic_memory/schemas/cloud.py +50 -0
  109. basic_memory/schemas/directory.py +30 -0
  110. basic_memory/schemas/importer.py +35 -0
  111. basic_memory/schemas/memory.py +187 -25
  112. basic_memory/schemas/project_info.py +211 -0
  113. basic_memory/schemas/prompt.py +90 -0
  114. basic_memory/schemas/request.py +56 -2
  115. basic_memory/schemas/response.py +1 -1
  116. basic_memory/schemas/search.py +31 -35
  117. basic_memory/schemas/sync_report.py +72 -0
  118. basic_memory/services/__init__.py +2 -1
  119. basic_memory/services/context_service.py +241 -104
  120. basic_memory/services/directory_service.py +295 -0
  121. basic_memory/services/entity_service.py +590 -60
  122. basic_memory/services/exceptions.py +21 -0
  123. basic_memory/services/file_service.py +284 -30
  124. basic_memory/services/initialization.py +191 -0
  125. basic_memory/services/link_resolver.py +49 -56
  126. basic_memory/services/project_service.py +863 -0
  127. basic_memory/services/search_service.py +168 -32
  128. basic_memory/sync/__init__.py +3 -2
  129. basic_memory/sync/background_sync.py +26 -0
  130. basic_memory/sync/sync_service.py +1180 -109
  131. basic_memory/sync/watch_service.py +412 -135
  132. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  133. basic_memory/templates/prompts/search.hbs +101 -0
  134. basic_memory/utils.py +383 -51
  135. basic_memory-0.16.1.dist-info/METADATA +493 -0
  136. basic_memory-0.16.1.dist-info/RECORD +148 -0
  137. {basic_memory-0.7.0.dist-info → basic_memory-0.16.1.dist-info}/entry_points.txt +1 -0
  138. basic_memory/alembic/README +0 -1
  139. basic_memory/cli/commands/sync.py +0 -206
  140. basic_memory/cli/commands/tools.py +0 -157
  141. basic_memory/mcp/tools/knowledge.py +0 -68
  142. basic_memory/mcp/tools/memory.py +0 -170
  143. basic_memory/mcp/tools/notes.py +0 -202
  144. basic_memory/schemas/discovery.py +0 -28
  145. basic_memory/sync/file_change_scanner.py +0 -158
  146. basic_memory/sync/utils.py +0 -31
  147. basic_memory-0.7.0.dist-info/METADATA +0 -378
  148. basic_memory-0.7.0.dist-info/RECORD +0 -82
  149. {basic_memory-0.7.0.dist-info → basic_memory-0.16.1.dist-info}/WHEEL +0 -0
  150. {basic_memory-0.7.0.dist-info → basic_memory-0.16.1.dist-info}/licenses/LICENSE +0 -0
basic_memory/config.py CHANGED
@@ -1,62 +1,516 @@
1
1
  """Configuration management for basic-memory."""
2
2
 
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
3
7
  from pathlib import Path
4
- from typing import Literal
8
+ from typing import Any, Dict, Literal, Optional, List, Tuple
5
9
 
6
- from pydantic import Field, field_validator
10
+ from loguru import logger
11
+ from pydantic import BaseModel, Field, field_validator
7
12
  from pydantic_settings import BaseSettings, SettingsConfigDict
8
13
 
14
+ import basic_memory
15
+ from basic_memory.utils import setup_logging, generate_permalink
16
+
17
+
9
18
  DATABASE_NAME = "memory.db"
19
+ APP_DATABASE_NAME = "memory.db" # Using the same name but in the app directory
10
20
  DATA_DIR_NAME = ".basic-memory"
21
+ CONFIG_FILE_NAME = "config.json"
22
+ WATCH_STATUS_JSON = "watch-status.json"
11
23
 
12
24
  Environment = Literal["test", "dev", "user"]
13
25
 
14
26
 
15
- class ProjectConfig(BaseSettings):
27
+ @dataclass
28
+ class ProjectConfig:
16
29
  """Configuration for a specific basic-memory project."""
17
30
 
31
+ name: str
32
+ home: Path
33
+
34
+ @property
35
+ def project(self):
36
+ return self.name
37
+
38
+ @property
39
+ def project_url(self) -> str: # pragma: no cover
40
+ return f"/{generate_permalink(self.name)}"
41
+
42
+
43
+ class CloudProjectConfig(BaseModel):
44
+ """Sync configuration for a cloud project.
45
+
46
+ This tracks the local working directory and sync state for a project
47
+ that is synced with Basic Memory Cloud.
48
+ """
49
+
50
+ local_path: str = Field(description="Local working directory path for this cloud project")
51
+ last_sync: Optional[datetime] = Field(
52
+ default=None, description="Timestamp of last successful sync operation"
53
+ )
54
+ bisync_initialized: bool = Field(
55
+ default=False, description="Whether rclone bisync baseline has been established"
56
+ )
57
+
58
+
59
+ class BasicMemoryConfig(BaseSettings):
60
+ """Pydantic model for Basic Memory global configuration."""
61
+
18
62
  env: Environment = Field(default="dev", description="Environment name")
19
63
 
20
- # Default to ~/basic-memory but allow override with env var: BASIC_MEMORY_HOME
21
- home: Path = Field(
22
- default_factory=lambda: Path.home() / "basic-memory",
23
- description="Base path for basic-memory files",
64
+ projects: Dict[str, str] = Field(
65
+ default_factory=lambda: {
66
+ "main": Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory")).as_posix()
67
+ },
68
+ description="Mapping of project names to their filesystem paths",
69
+ )
70
+ default_project: str = Field(
71
+ default="main",
72
+ description="Name of the default project to use",
73
+ )
74
+ default_project_mode: bool = Field(
75
+ default=False,
76
+ description="When True, MCP tools automatically use default_project when no project parameter is specified. Enables simplified UX for single-project workflows.",
24
77
  )
25
78
 
26
- # Name of the project
27
- project: str = Field(default="default", description="Project name")
79
+ # overridden by ~/.basic-memory/config.json
80
+ log_level: str = "INFO"
28
81
 
29
82
  # Watch service configuration
30
83
  sync_delay: int = Field(
31
- default=500, description="Milliseconds to wait after changes before syncing", gt=0
84
+ default=1000, description="Milliseconds to wait after changes before syncing", gt=0
32
85
  )
33
86
 
34
- log_level: str = "INFO"
87
+ watch_project_reload_interval: int = Field(
88
+ default=30, description="Seconds between reloading project list in watch service", gt=0
89
+ )
90
+
91
+ # update permalinks on move
92
+ update_permalinks_on_move: bool = Field(
93
+ default=False,
94
+ description="Whether to update permalinks when files are moved or renamed. default (False)",
95
+ )
96
+
97
+ sync_changes: bool = Field(
98
+ default=True,
99
+ description="Whether to sync changes in real time. default (True)",
100
+ )
101
+
102
+ sync_thread_pool_size: int = Field(
103
+ default=4,
104
+ description="Size of thread pool for file I/O operations in sync service. Default of 4 is optimized for cloud deployments with 1-2GB RAM.",
105
+ gt=0,
106
+ )
107
+
108
+ sync_max_concurrent_files: int = Field(
109
+ default=10,
110
+ description="Maximum number of files to process concurrently during sync. Limits memory usage on large projects (2000+ files). Lower values reduce memory consumption.",
111
+ gt=0,
112
+ )
113
+
114
+ kebab_filenames: bool = Field(
115
+ default=False,
116
+ description="Format for generated filenames. False preserves spaces and special chars, True converts them to hyphens for consistency with permalinks",
117
+ )
118
+
119
+ disable_permalinks: bool = Field(
120
+ default=False,
121
+ description="Disable automatic permalink generation in frontmatter. When enabled, new notes won't have permalinks added and sync won't update permalinks. Existing permalinks will still work for reading.",
122
+ )
123
+
124
+ skip_initialization_sync: bool = Field(
125
+ default=False,
126
+ description="Skip expensive initialization synchronization. Useful for cloud/stateless deployments where project reconciliation is not needed.",
127
+ )
128
+
129
+ # Project path constraints
130
+ project_root: Optional[str] = Field(
131
+ default=None,
132
+ description="If set, all projects must be created underneath this directory. Paths will be sanitized and constrained to this root. If not set, projects can be created anywhere (default behavior).",
133
+ )
134
+
135
+ # Cloud configuration
136
+ cloud_client_id: str = Field(
137
+ default="client_01K6KWQPW6J1M8VV7R3TZP5A6M",
138
+ description="OAuth client ID for Basic Memory Cloud",
139
+ )
140
+
141
+ cloud_domain: str = Field(
142
+ default="https://eloquent-lotus-05.authkit.app",
143
+ description="AuthKit domain for Basic Memory Cloud",
144
+ )
145
+
146
+ cloud_host: str = Field(
147
+ default_factory=lambda: os.getenv(
148
+ "BASIC_MEMORY_CLOUD_HOST", "https://cloud.basicmemory.com"
149
+ ),
150
+ description="Basic Memory Cloud host URL",
151
+ )
152
+
153
+ cloud_mode: bool = Field(
154
+ default=False,
155
+ description="Enable cloud mode - all requests go to cloud instead of local (config file value)",
156
+ )
157
+
158
+ cloud_projects: Dict[str, CloudProjectConfig] = Field(
159
+ default_factory=dict,
160
+ description="Cloud project sync configuration mapping project names to their local paths and sync state",
161
+ )
162
+
163
+ @property
164
+ def cloud_mode_enabled(self) -> bool:
165
+ """Check if cloud mode is enabled.
166
+
167
+ Priority:
168
+ 1. BASIC_MEMORY_CLOUD_MODE environment variable
169
+ 2. Config file value (cloud_mode)
170
+ """
171
+ env_value = os.environ.get("BASIC_MEMORY_CLOUD_MODE", "").lower()
172
+ if env_value in ("true", "1", "yes"):
173
+ return True
174
+ elif env_value in ("false", "0", "no"):
175
+ return False
176
+ # Fall back to config file value
177
+ return self.cloud_mode
35
178
 
36
179
  model_config = SettingsConfigDict(
37
180
  env_prefix="BASIC_MEMORY_",
38
181
  extra="ignore",
39
- env_file=".env",
40
- env_file_encoding="utf-8",
41
182
  )
42
183
 
184
+ def get_project_path(self, project_name: Optional[str] = None) -> Path: # pragma: no cover
185
+ """Get the path for a specific project or the default project."""
186
+ name = project_name or self.default_project
187
+
188
+ if name not in self.projects:
189
+ raise ValueError(f"Project '{name}' not found in configuration")
190
+
191
+ return Path(self.projects[name])
192
+
193
+ def model_post_init(self, __context: Any) -> None:
194
+ """Ensure configuration is valid after initialization."""
195
+ # Ensure main project exists
196
+ if "main" not in self.projects: # pragma: no cover
197
+ self.projects["main"] = (
198
+ Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory"))
199
+ ).as_posix()
200
+
201
+ # Ensure default project is valid
202
+ if self.default_project not in self.projects: # pragma: no cover
203
+ self.default_project = "main"
204
+
43
205
  @property
44
- def database_path(self) -> Path:
45
- """Get SQLite database path."""
46
- database_path = self.home / DATA_DIR_NAME / DATABASE_NAME
47
- if not database_path.exists():
206
+ def app_database_path(self) -> Path:
207
+ """Get the path to the app-level database.
208
+
209
+ This is the single database that will store all knowledge data
210
+ across all projects.
211
+ """
212
+ database_path = Path.home() / DATA_DIR_NAME / APP_DATABASE_NAME
213
+ if not database_path.exists(): # pragma: no cover
48
214
  database_path.parent.mkdir(parents=True, exist_ok=True)
49
215
  database_path.touch()
50
216
  return database_path
51
217
 
52
- @field_validator("home")
218
+ @property
219
+ def database_path(self) -> Path:
220
+ """Get SQLite database path.
221
+
222
+ Rreturns the app-level database path
223
+ for backward compatibility in the codebase.
224
+ """
225
+
226
+ # Load the app-level database path from the global config
227
+ config_manager = ConfigManager()
228
+ config = config_manager.load_config() # pragma: no cover
229
+ return config.app_database_path # pragma: no cover
230
+
231
+ @property
232
+ def project_list(self) -> List[ProjectConfig]: # pragma: no cover
233
+ """Get all configured projects as ProjectConfig objects."""
234
+ return [ProjectConfig(name=name, home=Path(path)) for name, path in self.projects.items()]
235
+
236
+ @field_validator("projects")
53
237
  @classmethod
54
- def ensure_path_exists(cls, v: Path) -> Path: # pragma: no cover
238
+ def ensure_project_paths_exists(cls, v: Dict[str, str]) -> Dict[str, str]: # pragma: no cover
55
239
  """Ensure project path exists."""
56
- if not v.exists():
57
- v.mkdir(parents=True)
240
+ for name, path_value in v.items():
241
+ path = Path(path_value)
242
+ if not Path(path).exists():
243
+ try:
244
+ path.mkdir(parents=True)
245
+ except Exception as e:
246
+ logger.error(f"Failed to create project path: {e}")
247
+ raise e
58
248
  return v
59
249
 
250
+ @property
251
+ def data_dir_path(self):
252
+ return Path.home() / DATA_DIR_NAME
253
+
254
+
255
+ # Module-level cache for configuration
256
+ _CONFIG_CACHE: Optional[BasicMemoryConfig] = None
257
+
258
+
259
+ class ConfigManager:
260
+ """Manages Basic Memory configuration."""
261
+
262
+ def __init__(self) -> None:
263
+ """Initialize the configuration manager."""
264
+ home = os.getenv("HOME", Path.home())
265
+ if isinstance(home, str):
266
+ home = Path(home)
267
+
268
+ # Allow override via environment variable
269
+ if config_dir := os.getenv("BASIC_MEMORY_CONFIG_DIR"):
270
+ self.config_dir = Path(config_dir)
271
+ else:
272
+ self.config_dir = home / DATA_DIR_NAME
273
+
274
+ self.config_file = self.config_dir / CONFIG_FILE_NAME
275
+
276
+ # Ensure config directory exists
277
+ self.config_dir.mkdir(parents=True, exist_ok=True)
278
+
279
+ @property
280
+ def config(self) -> BasicMemoryConfig:
281
+ """Get configuration, loading it lazily if needed."""
282
+ return self.load_config()
283
+
284
+ def load_config(self) -> BasicMemoryConfig:
285
+ """Load configuration from file or create default.
286
+
287
+ Environment variables take precedence over file config values,
288
+ following Pydantic Settings best practices.
289
+
290
+ Uses module-level cache for performance across ConfigManager instances.
291
+ """
292
+ global _CONFIG_CACHE
293
+
294
+ # Return cached config if available
295
+ if _CONFIG_CACHE is not None:
296
+ return _CONFIG_CACHE
297
+
298
+ if self.config_file.exists():
299
+ try:
300
+ file_data = json.loads(self.config_file.read_text(encoding="utf-8"))
301
+
302
+ # First, create config from environment variables (Pydantic will read them)
303
+ # Then overlay with file data for fields that aren't set via env vars
304
+ # This ensures env vars take precedence
305
+
306
+ # Get env-based config fields that are actually set
307
+ env_config = BasicMemoryConfig()
308
+ env_dict = env_config.model_dump()
309
+
310
+ # Merge: file data as base, but only use it for fields not set by env
311
+ # We detect env-set fields by comparing to default values
312
+ merged_data = file_data.copy()
313
+
314
+ # For fields that have env var overrides, use those instead of file values
315
+ # The env_prefix is "BASIC_MEMORY_" so we check those
316
+ for field_name in BasicMemoryConfig.model_fields.keys():
317
+ env_var_name = f"BASIC_MEMORY_{field_name.upper()}"
318
+ if env_var_name in os.environ:
319
+ # Environment variable is set, use it
320
+ merged_data[field_name] = env_dict[field_name]
321
+
322
+ _CONFIG_CACHE = BasicMemoryConfig(**merged_data)
323
+ return _CONFIG_CACHE
324
+ except Exception as e: # pragma: no cover
325
+ logger.exception(f"Failed to load config: {e}")
326
+ raise e
327
+ else:
328
+ config = BasicMemoryConfig()
329
+ self.save_config(config)
330
+ return config
331
+
332
+ def save_config(self, config: BasicMemoryConfig) -> None:
333
+ """Save configuration to file and invalidate cache."""
334
+ global _CONFIG_CACHE
335
+ save_basic_memory_config(self.config_file, config)
336
+ # Invalidate cache so next load_config() reads fresh data
337
+ _CONFIG_CACHE = None
338
+
339
+ @property
340
+ def projects(self) -> Dict[str, str]:
341
+ """Get all configured projects."""
342
+ return self.config.projects.copy()
343
+
344
+ @property
345
+ def default_project(self) -> str:
346
+ """Get the default project name."""
347
+ return self.config.default_project
348
+
349
+ def add_project(self, name: str, path: str) -> ProjectConfig:
350
+ """Add a new project to the configuration."""
351
+ project_name, _ = self.get_project(name)
352
+ if project_name: # pragma: no cover
353
+ raise ValueError(f"Project '{name}' already exists")
354
+
355
+ # Ensure the path exists
356
+ project_path = Path(path)
357
+ project_path.mkdir(parents=True, exist_ok=True) # pragma: no cover
358
+
359
+ # Load config, modify it, and save it
360
+ config = self.load_config()
361
+ config.projects[name] = project_path.as_posix()
362
+ self.save_config(config)
363
+ return ProjectConfig(name=name, home=project_path)
364
+
365
+ def remove_project(self, name: str) -> None:
366
+ """Remove a project from the configuration."""
367
+
368
+ project_name, path = self.get_project(name)
369
+ if not project_name: # pragma: no cover
370
+ raise ValueError(f"Project '{name}' not found")
371
+
372
+ # Load config, check, modify, and save
373
+ config = self.load_config()
374
+ if project_name == config.default_project: # pragma: no cover
375
+ raise ValueError(f"Cannot remove the default project '{name}'")
376
+
377
+ # Use the found project_name (which may differ from input name due to permalink matching)
378
+ del config.projects[project_name]
379
+ self.save_config(config)
380
+
381
+ def set_default_project(self, name: str) -> None:
382
+ """Set the default project."""
383
+ project_name, path = self.get_project(name)
384
+ if not project_name: # pragma: no cover
385
+ raise ValueError(f"Project '{name}' not found")
386
+
387
+ # Load config, modify, and save
388
+ config = self.load_config()
389
+ config.default_project = project_name
390
+ self.save_config(config)
391
+
392
+ def get_project(self, name: str) -> Tuple[str, str] | Tuple[None, None]:
393
+ """Look up a project from the configuration by name or permalink"""
394
+ project_permalink = generate_permalink(name)
395
+ app_config = self.config
396
+ for project_name, path in app_config.projects.items():
397
+ if project_permalink == generate_permalink(project_name):
398
+ return project_name, path
399
+ return None, None
400
+
401
+
402
+ def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
403
+ """
404
+ Get the project configuration for the current session.
405
+ If project_name is provided, it will be used instead of the default project.
406
+ """
407
+
408
+ actual_project_name = None
409
+
410
+ # load the config from file
411
+ config_manager = ConfigManager()
412
+ app_config = config_manager.load_config()
413
+
414
+ # Get project name from environment variable
415
+ os_project_name = os.environ.get("BASIC_MEMORY_PROJECT", None)
416
+ if os_project_name: # pragma: no cover
417
+ logger.warning(
418
+ f"BASIC_MEMORY_PROJECT is not supported anymore. Set the default project in the config instead. Setting default project to {os_project_name}"
419
+ )
420
+ actual_project_name = project_name
421
+ # if the project_name is passed in, use it
422
+ elif not project_name:
423
+ # use default
424
+ actual_project_name = app_config.default_project
425
+ else: # pragma: no cover
426
+ actual_project_name = project_name
427
+
428
+ # the config contains a dict[str,str] of project names and absolute paths
429
+ assert actual_project_name is not None, "actual_project_name cannot be None"
430
+
431
+ project_permalink = generate_permalink(actual_project_name)
432
+
433
+ for name, path in app_config.projects.items():
434
+ if project_permalink == generate_permalink(name):
435
+ return ProjectConfig(name=name, home=Path(path))
436
+
437
+ # otherwise raise error
438
+ raise ValueError(f"Project '{actual_project_name}' not found") # pragma: no cover
439
+
440
+
441
+ def save_basic_memory_config(file_path: Path, config: BasicMemoryConfig) -> None:
442
+ """Save configuration to file."""
443
+ try:
444
+ # Use model_dump with mode='json' to serialize datetime objects properly
445
+ config_dict = config.model_dump(mode="json")
446
+ file_path.write_text(json.dumps(config_dict, indent=2))
447
+ except Exception as e: # pragma: no cover
448
+ logger.error(f"Failed to save config: {e}")
449
+
450
+
451
+ # setup logging to a single log file in user home directory
452
+ user_home = Path.home()
453
+ log_dir = user_home / DATA_DIR_NAME
454
+ log_dir.mkdir(parents=True, exist_ok=True)
455
+
456
+
457
+ # Process info for logging
458
+ def get_process_name(): # pragma: no cover
459
+ """
460
+ get the type of process for logging
461
+ """
462
+ import sys
463
+
464
+ if "sync" in sys.argv:
465
+ return "sync"
466
+ elif "mcp" in sys.argv:
467
+ return "mcp"
468
+ elif "cli" in sys.argv:
469
+ return "cli"
470
+ else:
471
+ return "api"
472
+
473
+
474
+ process_name = get_process_name()
475
+
476
+ # Global flag to track if logging has been set up
477
+ _LOGGING_SETUP = False
478
+
479
+
480
+ # Logging
481
+
482
+
483
+ def setup_basic_memory_logging(): # pragma: no cover
484
+ """Set up logging for basic-memory, ensuring it only happens once."""
485
+ global _LOGGING_SETUP
486
+ if _LOGGING_SETUP:
487
+ # We can't log before logging is set up
488
+ # print("Skipping duplicate logging setup")
489
+ return
490
+
491
+ # Check for console logging environment variable - accept more truthy values
492
+ console_logging_env = os.getenv("BASIC_MEMORY_CONSOLE_LOGGING", "false").lower()
493
+ console_logging = console_logging_env in ("true", "1", "yes", "on")
494
+
495
+ # Check for log level environment variable first, fall back to config
496
+ log_level = os.getenv("BASIC_MEMORY_LOG_LEVEL")
497
+ if not log_level:
498
+ config_manager = ConfigManager()
499
+ log_level = config_manager.config.log_level
500
+
501
+ config_manager = ConfigManager()
502
+ config = get_project_config()
503
+ setup_logging(
504
+ env=config_manager.config.env,
505
+ home_dir=user_home, # Use user home for logs
506
+ log_level=log_level,
507
+ log_file=f"{DATA_DIR_NAME}/basic-memory-{process_name}.log",
508
+ console=console_logging,
509
+ )
510
+
511
+ logger.info(f"Basic Memory {basic_memory.__version__} (Project: {config.project})")
512
+ _LOGGING_SETUP = True
513
+
60
514
 
61
- # Load project config
62
- config = ProjectConfig()
515
+ # Set up logging
516
+ setup_basic_memory_logging()