basic-memory 0.16.1__py3-none-any.whl → 0.17.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 (143) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/env.py +112 -26
  3. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  4. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +15 -3
  5. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +44 -36
  6. basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
  7. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  8. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +13 -0
  9. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  10. basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
  11. basic_memory/api/app.py +45 -24
  12. basic_memory/api/container.py +133 -0
  13. basic_memory/api/routers/knowledge_router.py +17 -5
  14. basic_memory/api/routers/project_router.py +68 -14
  15. basic_memory/api/routers/resource_router.py +37 -27
  16. basic_memory/api/routers/utils.py +53 -14
  17. basic_memory/api/v2/__init__.py +35 -0
  18. basic_memory/api/v2/routers/__init__.py +21 -0
  19. basic_memory/api/v2/routers/directory_router.py +93 -0
  20. basic_memory/api/v2/routers/importer_router.py +181 -0
  21. basic_memory/api/v2/routers/knowledge_router.py +427 -0
  22. basic_memory/api/v2/routers/memory_router.py +130 -0
  23. basic_memory/api/v2/routers/project_router.py +359 -0
  24. basic_memory/api/v2/routers/prompt_router.py +269 -0
  25. basic_memory/api/v2/routers/resource_router.py +286 -0
  26. basic_memory/api/v2/routers/search_router.py +73 -0
  27. basic_memory/cli/app.py +43 -7
  28. basic_memory/cli/auth.py +27 -4
  29. basic_memory/cli/commands/__init__.py +3 -1
  30. basic_memory/cli/commands/cloud/api_client.py +20 -5
  31. basic_memory/cli/commands/cloud/cloud_utils.py +13 -6
  32. basic_memory/cli/commands/cloud/rclone_commands.py +110 -14
  33. basic_memory/cli/commands/cloud/rclone_installer.py +18 -4
  34. basic_memory/cli/commands/cloud/upload.py +10 -3
  35. basic_memory/cli/commands/command_utils.py +52 -4
  36. basic_memory/cli/commands/db.py +78 -19
  37. basic_memory/cli/commands/format.py +198 -0
  38. basic_memory/cli/commands/import_chatgpt.py +12 -8
  39. basic_memory/cli/commands/import_claude_conversations.py +12 -8
  40. basic_memory/cli/commands/import_claude_projects.py +12 -8
  41. basic_memory/cli/commands/import_memory_json.py +12 -8
  42. basic_memory/cli/commands/mcp.py +8 -26
  43. basic_memory/cli/commands/project.py +22 -9
  44. basic_memory/cli/commands/status.py +3 -2
  45. basic_memory/cli/commands/telemetry.py +81 -0
  46. basic_memory/cli/container.py +84 -0
  47. basic_memory/cli/main.py +7 -0
  48. basic_memory/config.py +177 -77
  49. basic_memory/db.py +183 -77
  50. basic_memory/deps/__init__.py +293 -0
  51. basic_memory/deps/config.py +26 -0
  52. basic_memory/deps/db.py +56 -0
  53. basic_memory/deps/importers.py +200 -0
  54. basic_memory/deps/projects.py +238 -0
  55. basic_memory/deps/repositories.py +179 -0
  56. basic_memory/deps/services.py +480 -0
  57. basic_memory/deps.py +14 -409
  58. basic_memory/file_utils.py +212 -3
  59. basic_memory/ignore_utils.py +5 -5
  60. basic_memory/importers/base.py +40 -19
  61. basic_memory/importers/chatgpt_importer.py +17 -4
  62. basic_memory/importers/claude_conversations_importer.py +27 -12
  63. basic_memory/importers/claude_projects_importer.py +50 -14
  64. basic_memory/importers/memory_json_importer.py +36 -16
  65. basic_memory/importers/utils.py +5 -2
  66. basic_memory/markdown/entity_parser.py +62 -23
  67. basic_memory/markdown/markdown_processor.py +67 -4
  68. basic_memory/markdown/plugins.py +4 -2
  69. basic_memory/markdown/utils.py +10 -1
  70. basic_memory/mcp/async_client.py +1 -0
  71. basic_memory/mcp/clients/__init__.py +28 -0
  72. basic_memory/mcp/clients/directory.py +70 -0
  73. basic_memory/mcp/clients/knowledge.py +176 -0
  74. basic_memory/mcp/clients/memory.py +120 -0
  75. basic_memory/mcp/clients/project.py +89 -0
  76. basic_memory/mcp/clients/resource.py +71 -0
  77. basic_memory/mcp/clients/search.py +65 -0
  78. basic_memory/mcp/container.py +110 -0
  79. basic_memory/mcp/project_context.py +47 -33
  80. basic_memory/mcp/prompts/ai_assistant_guide.py +2 -2
  81. basic_memory/mcp/prompts/recent_activity.py +2 -2
  82. basic_memory/mcp/prompts/utils.py +3 -3
  83. basic_memory/mcp/server.py +58 -0
  84. basic_memory/mcp/tools/build_context.py +14 -14
  85. basic_memory/mcp/tools/canvas.py +34 -12
  86. basic_memory/mcp/tools/chatgpt_tools.py +4 -1
  87. basic_memory/mcp/tools/delete_note.py +31 -7
  88. basic_memory/mcp/tools/edit_note.py +14 -9
  89. basic_memory/mcp/tools/list_directory.py +7 -17
  90. basic_memory/mcp/tools/move_note.py +35 -31
  91. basic_memory/mcp/tools/project_management.py +29 -25
  92. basic_memory/mcp/tools/read_content.py +13 -3
  93. basic_memory/mcp/tools/read_note.py +24 -14
  94. basic_memory/mcp/tools/recent_activity.py +32 -38
  95. basic_memory/mcp/tools/search.py +17 -10
  96. basic_memory/mcp/tools/utils.py +28 -0
  97. basic_memory/mcp/tools/view_note.py +2 -1
  98. basic_memory/mcp/tools/write_note.py +37 -14
  99. basic_memory/models/knowledge.py +15 -2
  100. basic_memory/models/project.py +7 -1
  101. basic_memory/models/search.py +58 -2
  102. basic_memory/project_resolver.py +222 -0
  103. basic_memory/repository/entity_repository.py +210 -3
  104. basic_memory/repository/observation_repository.py +1 -0
  105. basic_memory/repository/postgres_search_repository.py +451 -0
  106. basic_memory/repository/project_repository.py +38 -1
  107. basic_memory/repository/relation_repository.py +58 -2
  108. basic_memory/repository/repository.py +1 -0
  109. basic_memory/repository/search_index_row.py +95 -0
  110. basic_memory/repository/search_repository.py +77 -615
  111. basic_memory/repository/search_repository_base.py +241 -0
  112. basic_memory/repository/sqlite_search_repository.py +437 -0
  113. basic_memory/runtime.py +61 -0
  114. basic_memory/schemas/base.py +36 -6
  115. basic_memory/schemas/directory.py +2 -1
  116. basic_memory/schemas/memory.py +9 -2
  117. basic_memory/schemas/project_info.py +2 -0
  118. basic_memory/schemas/response.py +84 -27
  119. basic_memory/schemas/search.py +5 -0
  120. basic_memory/schemas/sync_report.py +1 -1
  121. basic_memory/schemas/v2/__init__.py +27 -0
  122. basic_memory/schemas/v2/entity.py +133 -0
  123. basic_memory/schemas/v2/resource.py +47 -0
  124. basic_memory/services/context_service.py +219 -43
  125. basic_memory/services/directory_service.py +26 -11
  126. basic_memory/services/entity_service.py +68 -33
  127. basic_memory/services/file_service.py +131 -16
  128. basic_memory/services/initialization.py +51 -26
  129. basic_memory/services/link_resolver.py +1 -0
  130. basic_memory/services/project_service.py +68 -43
  131. basic_memory/services/search_service.py +75 -16
  132. basic_memory/sync/__init__.py +2 -1
  133. basic_memory/sync/coordinator.py +160 -0
  134. basic_memory/sync/sync_service.py +135 -115
  135. basic_memory/sync/watch_service.py +32 -12
  136. basic_memory/telemetry.py +249 -0
  137. basic_memory/utils.py +96 -75
  138. {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/METADATA +129 -5
  139. basic_memory-0.17.4.dist-info/RECORD +193 -0
  140. {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
  141. basic_memory-0.16.1.dist-info/RECORD +0 -148
  142. {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +0 -0
  143. {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,81 @@
1
+ """Telemetry commands for basic-memory CLI."""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.panel import Panel
6
+
7
+ from basic_memory.cli.app import app
8
+ from basic_memory.config import ConfigManager
9
+
10
+ console = Console()
11
+
12
+ # Create telemetry subcommand group
13
+ telemetry_app = typer.Typer(help="Manage anonymous telemetry settings")
14
+ app.add_typer(telemetry_app, name="telemetry")
15
+
16
+
17
+ @telemetry_app.command("enable")
18
+ def enable() -> None:
19
+ """Enable anonymous telemetry.
20
+
21
+ Telemetry helps improve Basic Memory by collecting anonymous usage data.
22
+ No personal data, note content, or file paths are ever collected.
23
+ """
24
+ config_manager = ConfigManager()
25
+ config = config_manager.config
26
+ config.telemetry_enabled = True
27
+ config_manager.save_config(config)
28
+ console.print("[green]Telemetry enabled[/green]")
29
+ console.print("[dim]Thank you for helping improve Basic Memory![/dim]")
30
+
31
+
32
+ @telemetry_app.command("disable")
33
+ def disable() -> None:
34
+ """Disable anonymous telemetry.
35
+
36
+ You can re-enable telemetry anytime with: bm telemetry enable
37
+ """
38
+ config_manager = ConfigManager()
39
+ config = config_manager.config
40
+ config.telemetry_enabled = False
41
+ config_manager.save_config(config)
42
+ console.print("[yellow]Telemetry disabled[/yellow]")
43
+
44
+
45
+ @telemetry_app.command("status")
46
+ def status() -> None:
47
+ """Show current telemetry status and what's collected."""
48
+ from basic_memory.telemetry import get_install_id, TELEMETRY_DOCS_URL
49
+
50
+ config = ConfigManager().config
51
+
52
+ status_text = (
53
+ "[green]enabled[/green]" if config.telemetry_enabled else "[yellow]disabled[/yellow]"
54
+ )
55
+
56
+ console.print(f"\nTelemetry: {status_text}")
57
+ console.print(f"Install ID: [dim]{get_install_id()}[/dim]")
58
+ console.print()
59
+
60
+ what_we_collect = """
61
+ [bold]What we collect:[/bold]
62
+ - App version, Python version, OS, architecture
63
+ - Feature usage (which MCP tools and CLI commands)
64
+ - Sync statistics (entity count, duration)
65
+ - Error types (sanitized, no file paths)
66
+
67
+ [bold]What we NEVER collect:[/bold]
68
+ - Note content, file names, or paths
69
+ - Personal information
70
+ - IP addresses
71
+ """
72
+
73
+ console.print(
74
+ Panel(
75
+ what_we_collect.strip(),
76
+ title="Telemetry Details",
77
+ border_style="blue",
78
+ expand=False,
79
+ )
80
+ )
81
+ console.print(f"[dim]Details: {TELEMETRY_DOCS_URL}[/dim]")
@@ -0,0 +1,84 @@
1
+ """CLI composition root for Basic Memory.
2
+
3
+ This container owns reading ConfigManager and environment variables for the
4
+ CLI entrypoint. Downstream modules receive config/dependencies explicitly
5
+ rather than reading globals.
6
+
7
+ Design principles:
8
+ - Only this module reads ConfigManager directly
9
+ - Runtime mode (cloud/local/test) is resolved here
10
+ - Different CLI commands may need different initialization
11
+ """
12
+
13
+ from dataclasses import dataclass
14
+
15
+ from basic_memory.config import BasicMemoryConfig, ConfigManager
16
+ from basic_memory.runtime import RuntimeMode, resolve_runtime_mode
17
+
18
+
19
+ @dataclass
20
+ class CliContainer:
21
+ """Composition root for the CLI entrypoint.
22
+
23
+ Holds resolved configuration and runtime context.
24
+ Created once at CLI startup, then used by subcommands.
25
+ """
26
+
27
+ config: BasicMemoryConfig
28
+ mode: RuntimeMode
29
+
30
+ @classmethod
31
+ def create(cls) -> "CliContainer":
32
+ """Create container by reading ConfigManager.
33
+
34
+ This is the single point where CLI reads global config.
35
+ """
36
+ config = ConfigManager().config
37
+ mode = resolve_runtime_mode(
38
+ cloud_mode_enabled=config.cloud_mode_enabled,
39
+ is_test_env=config.is_test_env,
40
+ )
41
+ return cls(config=config, mode=mode)
42
+
43
+ # --- Runtime Mode Properties ---
44
+
45
+ @property
46
+ def is_cloud_mode(self) -> bool:
47
+ """Whether running in cloud mode."""
48
+ return self.mode.is_cloud
49
+
50
+
51
+ # Module-level container instance (set by app callback)
52
+ _container: CliContainer | None = None
53
+
54
+
55
+ def get_container() -> CliContainer:
56
+ """Get the current CLI container.
57
+
58
+ Returns:
59
+ The CLI container
60
+
61
+ Raises:
62
+ RuntimeError: If container hasn't been initialized
63
+ """
64
+ if _container is None:
65
+ raise RuntimeError("CLI container not initialized. Call set_container() first.")
66
+ return _container
67
+
68
+
69
+ def set_container(container: CliContainer) -> None:
70
+ """Set the CLI container (called by app callback)."""
71
+ global _container
72
+ _container = container
73
+
74
+
75
+ def get_or_create_container() -> CliContainer:
76
+ """Get existing container or create new one.
77
+
78
+ This is useful for CLI commands that might be called before
79
+ the main app callback runs (e.g., eager options).
80
+ """
81
+ global _container
82
+ if _container is None:
83
+ _container = CliContainer.create()
84
+ return _container
basic_memory/cli/main.py CHANGED
@@ -13,9 +13,16 @@ from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
13
13
  mcp,
14
14
  project,
15
15
  status,
16
+ telemetry,
16
17
  tool,
17
18
  )
18
19
 
20
+ # Re-apply warning filter AFTER all imports
21
+ # (authlib adds a DeprecationWarning filter that overrides ours)
22
+ import warnings # pragma: no cover
23
+
24
+ warnings.filterwarnings("ignore") # pragma: no cover
25
+
19
26
  if __name__ == "__main__": # pragma: no cover
20
27
  # start the app
21
28
  app()
basic_memory/config.py CHANGED
@@ -6,12 +6,12 @@ from dataclasses import dataclass
6
6
  from datetime import datetime
7
7
  from pathlib import Path
8
8
  from typing import Any, Dict, Literal, Optional, List, Tuple
9
+ from enum import Enum
9
10
 
10
11
  from loguru import logger
11
- from pydantic import BaseModel, Field, field_validator
12
+ from pydantic import BaseModel, Field, model_validator
12
13
  from pydantic_settings import BaseSettings, SettingsConfigDict
13
14
 
14
- import basic_memory
15
15
  from basic_memory.utils import setup_logging, generate_permalink
16
16
 
17
17
 
@@ -24,6 +24,13 @@ WATCH_STATUS_JSON = "watch-status.json"
24
24
  Environment = Literal["test", "dev", "user"]
25
25
 
26
26
 
27
+ class DatabaseBackend(str, Enum):
28
+ """Supported database backends."""
29
+
30
+ SQLITE = "sqlite"
31
+ POSTGRES = "postgres"
32
+
33
+
27
34
  @dataclass
28
35
  class ProjectConfig:
29
36
  """Configuration for a specific basic-memory project."""
@@ -33,7 +40,7 @@ class ProjectConfig:
33
40
 
34
41
  @property
35
42
  def project(self):
36
- return self.name
43
+ return self.name # pragma: no cover
37
44
 
38
45
  @property
39
46
  def project_url(self) -> str: # pragma: no cover
@@ -63,8 +70,10 @@ class BasicMemoryConfig(BaseSettings):
63
70
 
64
71
  projects: Dict[str, str] = Field(
65
72
  default_factory=lambda: {
66
- "main": Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory")).as_posix()
67
- },
73
+ "main": str(Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory")))
74
+ }
75
+ if os.getenv("BASIC_MEMORY_HOME")
76
+ else {},
68
77
  description="Mapping of project names to their filesystem paths",
69
78
  )
70
79
  default_project: str = Field(
@@ -79,13 +88,43 @@ class BasicMemoryConfig(BaseSettings):
79
88
  # overridden by ~/.basic-memory/config.json
80
89
  log_level: str = "INFO"
81
90
 
91
+ # Database configuration
92
+ database_backend: DatabaseBackend = Field(
93
+ default=DatabaseBackend.SQLITE,
94
+ description="Database backend to use (sqlite or postgres)",
95
+ )
96
+
97
+ database_url: Optional[str] = Field(
98
+ default=None,
99
+ description="Database connection URL. For Postgres, use postgresql+asyncpg://user:pass@host:port/db. If not set, SQLite will use default path.",
100
+ )
101
+
102
+ # Database connection pool configuration (Postgres only)
103
+ db_pool_size: int = Field(
104
+ default=20,
105
+ description="Number of connections to keep in the pool (Postgres only)",
106
+ gt=0,
107
+ )
108
+ db_pool_overflow: int = Field(
109
+ default=40,
110
+ description="Max additional connections beyond pool_size under load (Postgres only)",
111
+ gt=0,
112
+ )
113
+ db_pool_recycle: int = Field(
114
+ default=180,
115
+ description="Recycle connections after N seconds to prevent stale connections. Default 180s works well with Neon's ~5 minute scale-to-zero (Postgres only)",
116
+ gt=0,
117
+ )
118
+
82
119
  # Watch service configuration
83
120
  sync_delay: int = Field(
84
121
  default=1000, description="Milliseconds to wait after changes before syncing", gt=0
85
122
  )
86
123
 
87
124
  watch_project_reload_interval: int = Field(
88
- default=30, description="Seconds between reloading project list in watch service", gt=0
125
+ default=300,
126
+ description="Seconds between reloading project list in watch service. Higher values reduce CPU usage by minimizing watcher restarts. Default 300s (5 min) balances efficiency with responsiveness to new projects.",
127
+ gt=0,
89
128
  )
90
129
 
91
130
  # update permalinks on move
@@ -126,6 +165,28 @@ class BasicMemoryConfig(BaseSettings):
126
165
  description="Skip expensive initialization synchronization. Useful for cloud/stateless deployments where project reconciliation is not needed.",
127
166
  )
128
167
 
168
+ # File formatting configuration
169
+ format_on_save: bool = Field(
170
+ default=False,
171
+ description="Automatically format files after saving using configured formatter. Disabled by default.",
172
+ )
173
+
174
+ formatter_command: Optional[str] = Field(
175
+ default=None,
176
+ description="External formatter command. Use {file} as placeholder for file path. If not set, uses built-in mdformat (Python, no Node.js required). Set to 'npx prettier --write {file}' for Prettier.",
177
+ )
178
+
179
+ formatters: Dict[str, str] = Field(
180
+ default_factory=dict,
181
+ description="Per-extension formatters. Keys are extensions (without dot), values are commands. Example: {'md': 'prettier --write {file}', 'json': 'prettier --write {file}'}",
182
+ )
183
+
184
+ formatter_timeout: float = Field(
185
+ default=5.0,
186
+ description="Maximum seconds to wait for formatter to complete",
187
+ gt=0,
188
+ )
189
+
129
190
  # Project path constraints
130
191
  project_root: Optional[str] = Field(
131
192
  default=None,
@@ -160,6 +221,34 @@ class BasicMemoryConfig(BaseSettings):
160
221
  description="Cloud project sync configuration mapping project names to their local paths and sync state",
161
222
  )
162
223
 
224
+ # Telemetry configuration (Homebrew-style opt-out)
225
+ telemetry_enabled: bool = Field(
226
+ default=True,
227
+ description="Send anonymous usage statistics to help improve Basic Memory. Disable with: bm telemetry disable",
228
+ )
229
+
230
+ telemetry_notice_shown: bool = Field(
231
+ default=False,
232
+ description="Whether the one-time telemetry notice has been shown to the user",
233
+ )
234
+
235
+ @property
236
+ def is_test_env(self) -> bool:
237
+ """Check if running in a test environment.
238
+
239
+ Returns True if any of:
240
+ - env field is set to "test"
241
+ - BASIC_MEMORY_ENV environment variable is "test"
242
+ - PYTEST_CURRENT_TEST environment variable is set (pytest is running)
243
+
244
+ Used to disable features like telemetry and file watchers during tests.
245
+ """
246
+ return (
247
+ self.env == "test"
248
+ or os.getenv("BASIC_MEMORY_ENV", "").lower() == "test"
249
+ or os.getenv("PYTEST_CURRENT_TEST") is not None
250
+ )
251
+
163
252
  @property
164
253
  def cloud_mode_enabled(self) -> bool:
165
254
  """Check if cloud mode is enabled.
@@ -176,6 +265,36 @@ class BasicMemoryConfig(BaseSettings):
176
265
  # Fall back to config file value
177
266
  return self.cloud_mode
178
267
 
268
+ @classmethod
269
+ def for_cloud_tenant(
270
+ cls,
271
+ database_url: str,
272
+ projects: Optional[Dict[str, str]] = None,
273
+ ) -> "BasicMemoryConfig":
274
+ """Create config for cloud tenant - no config.json, database is source of truth.
275
+
276
+ This factory method creates a BasicMemoryConfig suitable for cloud deployments
277
+ where:
278
+ - Database is Postgres (Neon), not SQLite
279
+ - Projects are discovered from the database, not config file
280
+ - Path validation is skipped (no local filesystem in cloud)
281
+ - Initialization sync is skipped (stateless deployment)
282
+
283
+ Args:
284
+ database_url: Postgres connection URL for tenant database
285
+ projects: Optional project mapping (usually empty, discovered from DB)
286
+
287
+ Returns:
288
+ BasicMemoryConfig configured for cloud mode
289
+ """
290
+ return cls( # pragma: no cover
291
+ database_backend=DatabaseBackend.POSTGRES,
292
+ database_url=database_url,
293
+ projects=projects or {},
294
+ cloud_mode=True,
295
+ skip_initialization_sync=True,
296
+ )
297
+
179
298
  model_config = SettingsConfigDict(
180
299
  env_prefix="BASIC_MEMORY_",
181
300
  extra="ignore",
@@ -192,15 +311,20 @@ class BasicMemoryConfig(BaseSettings):
192
311
 
193
312
  def model_post_init(self, __context: Any) -> None:
194
313
  """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"] = (
314
+ # Skip project initialization in cloud mode - projects are discovered from DB
315
+ if self.database_backend == DatabaseBackend.POSTGRES: # pragma: no cover
316
+ return # pragma: no cover
317
+
318
+ # Ensure at least one project exists; if none exist then create main
319
+ if not self.projects: # pragma: no cover
320
+ self.projects["main"] = str(
198
321
  Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory"))
199
- ).as_posix()
322
+ )
200
323
 
201
- # Ensure default project is valid
324
+ # Ensure default project is valid (i.e. points to an existing project)
202
325
  if self.default_project not in self.projects: # pragma: no cover
203
- self.default_project = "main"
326
+ # Set default to first available project
327
+ self.default_project = next(iter(self.projects.keys()))
204
328
 
205
329
  @property
206
330
  def app_database_path(self) -> Path:
@@ -233,19 +357,26 @@ class BasicMemoryConfig(BaseSettings):
233
357
  """Get all configured projects as ProjectConfig objects."""
234
358
  return [ProjectConfig(name=name, home=Path(path)) for name, path in self.projects.items()]
235
359
 
236
- @field_validator("projects")
237
- @classmethod
238
- def ensure_project_paths_exists(cls, v: Dict[str, str]) -> Dict[str, str]: # pragma: no cover
239
- """Ensure project path exists."""
240
- for name, path_value in v.items():
360
+ @model_validator(mode="after")
361
+ def ensure_project_paths_exists(self) -> "BasicMemoryConfig": # pragma: no cover
362
+ """Ensure project paths exist.
363
+
364
+ Skips path creation when using Postgres backend (cloud mode) since
365
+ cloud tenants don't use local filesystem paths.
366
+ """
367
+ # Skip path creation for cloud mode - no local filesystem
368
+ if self.database_backend == DatabaseBackend.POSTGRES:
369
+ return self
370
+
371
+ for name, path_value in self.projects.items():
241
372
  path = Path(path_value)
242
- if not Path(path).exists():
373
+ if not path.exists():
243
374
  try:
244
375
  path.mkdir(parents=True)
245
376
  except Exception as e:
246
377
  logger.error(f"Failed to create project path: {e}")
247
378
  raise e
248
- return v
379
+ return self
249
380
 
250
381
  @property
251
382
  def data_dir_path(self):
@@ -358,7 +489,7 @@ class ConfigManager:
358
489
 
359
490
  # Load config, modify it, and save it
360
491
  config = self.load_config()
361
- config.projects[name] = project_path.as_posix()
492
+ config.projects[name] = str(project_path)
362
493
  self.save_config(config)
363
494
  return ProjectConfig(name=name, home=project_path)
364
495
 
@@ -448,69 +579,38 @@ def save_basic_memory_config(file_path: Path, config: BasicMemoryConfig) -> None
448
579
  logger.error(f"Failed to save config: {e}")
449
580
 
450
581
 
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)
582
+ # Logging initialization functions for different entry points
455
583
 
456
584
 
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
585
+ def init_cli_logging() -> None: # pragma: no cover
586
+ """Initialize logging for CLI commands - file only.
481
587
 
588
+ CLI commands should not log to stdout to avoid interfering with
589
+ command output and shell integration.
590
+ """
591
+ log_level = os.getenv("BASIC_MEMORY_LOG_LEVEL", "INFO")
592
+ setup_logging(log_level=log_level, log_to_file=True)
482
593
 
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
594
 
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
595
+ def init_mcp_logging() -> None: # pragma: no cover
596
+ """Initialize logging for MCP server - file only.
500
597
 
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
- )
598
+ MCP server must not log to stdout as it would corrupt the
599
+ JSON-RPC protocol communication.
600
+ """
601
+ log_level = os.getenv("BASIC_MEMORY_LOG_LEVEL", "INFO")
602
+ setup_logging(log_level=log_level, log_to_file=True)
510
603
 
511
- logger.info(f"Basic Memory {basic_memory.__version__} (Project: {config.project})")
512
- _LOGGING_SETUP = True
513
604
 
605
+ def init_api_logging() -> None: # pragma: no cover
606
+ """Initialize logging for API server.
514
607
 
515
- # Set up logging
516
- setup_basic_memory_logging()
608
+ Cloud mode (BASIC_MEMORY_CLOUD_MODE=1): stdout with structured context
609
+ Local mode: file only
610
+ """
611
+ log_level = os.getenv("BASIC_MEMORY_LOG_LEVEL", "INFO")
612
+ cloud_mode = os.getenv("BASIC_MEMORY_CLOUD_MODE", "").lower() in ("1", "true")
613
+ if cloud_mode:
614
+ setup_logging(log_level=log_level, log_to_stdout=True, structured_context=True)
615
+ else:
616
+ setup_logging(log_level=log_level, log_to_file=True)