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.
- basic_memory/__init__.py +1 -1
- basic_memory/alembic/env.py +112 -26
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +15 -3
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +44 -36
- basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
- basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +13 -0
- basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
- basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
- basic_memory/api/app.py +45 -24
- basic_memory/api/container.py +133 -0
- basic_memory/api/routers/knowledge_router.py +17 -5
- basic_memory/api/routers/project_router.py +68 -14
- basic_memory/api/routers/resource_router.py +37 -27
- basic_memory/api/routers/utils.py +53 -14
- basic_memory/api/v2/__init__.py +35 -0
- basic_memory/api/v2/routers/__init__.py +21 -0
- basic_memory/api/v2/routers/directory_router.py +93 -0
- basic_memory/api/v2/routers/importer_router.py +181 -0
- basic_memory/api/v2/routers/knowledge_router.py +427 -0
- basic_memory/api/v2/routers/memory_router.py +130 -0
- basic_memory/api/v2/routers/project_router.py +359 -0
- basic_memory/api/v2/routers/prompt_router.py +269 -0
- basic_memory/api/v2/routers/resource_router.py +286 -0
- basic_memory/api/v2/routers/search_router.py +73 -0
- basic_memory/cli/app.py +43 -7
- basic_memory/cli/auth.py +27 -4
- basic_memory/cli/commands/__init__.py +3 -1
- basic_memory/cli/commands/cloud/api_client.py +20 -5
- basic_memory/cli/commands/cloud/cloud_utils.py +13 -6
- basic_memory/cli/commands/cloud/rclone_commands.py +110 -14
- basic_memory/cli/commands/cloud/rclone_installer.py +18 -4
- basic_memory/cli/commands/cloud/upload.py +10 -3
- basic_memory/cli/commands/command_utils.py +52 -4
- basic_memory/cli/commands/db.py +78 -19
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +12 -8
- basic_memory/cli/commands/import_claude_conversations.py +12 -8
- basic_memory/cli/commands/import_claude_projects.py +12 -8
- basic_memory/cli/commands/import_memory_json.py +12 -8
- basic_memory/cli/commands/mcp.py +8 -26
- basic_memory/cli/commands/project.py +22 -9
- basic_memory/cli/commands/status.py +3 -2
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/container.py +84 -0
- basic_memory/cli/main.py +7 -0
- basic_memory/config.py +177 -77
- basic_memory/db.py +183 -77
- basic_memory/deps/__init__.py +293 -0
- basic_memory/deps/config.py +26 -0
- basic_memory/deps/db.py +56 -0
- basic_memory/deps/importers.py +200 -0
- basic_memory/deps/projects.py +238 -0
- basic_memory/deps/repositories.py +179 -0
- basic_memory/deps/services.py +480 -0
- basic_memory/deps.py +14 -409
- basic_memory/file_utils.py +212 -3
- basic_memory/ignore_utils.py +5 -5
- basic_memory/importers/base.py +40 -19
- basic_memory/importers/chatgpt_importer.py +17 -4
- basic_memory/importers/claude_conversations_importer.py +27 -12
- basic_memory/importers/claude_projects_importer.py +50 -14
- basic_memory/importers/memory_json_importer.py +36 -16
- basic_memory/importers/utils.py +5 -2
- basic_memory/markdown/entity_parser.py +62 -23
- basic_memory/markdown/markdown_processor.py +67 -4
- basic_memory/markdown/plugins.py +4 -2
- basic_memory/markdown/utils.py +10 -1
- basic_memory/mcp/async_client.py +1 -0
- basic_memory/mcp/clients/__init__.py +28 -0
- basic_memory/mcp/clients/directory.py +70 -0
- basic_memory/mcp/clients/knowledge.py +176 -0
- basic_memory/mcp/clients/memory.py +120 -0
- basic_memory/mcp/clients/project.py +89 -0
- basic_memory/mcp/clients/resource.py +71 -0
- basic_memory/mcp/clients/search.py +65 -0
- basic_memory/mcp/container.py +110 -0
- basic_memory/mcp/project_context.py +47 -33
- basic_memory/mcp/prompts/ai_assistant_guide.py +2 -2
- basic_memory/mcp/prompts/recent_activity.py +2 -2
- basic_memory/mcp/prompts/utils.py +3 -3
- basic_memory/mcp/server.py +58 -0
- basic_memory/mcp/tools/build_context.py +14 -14
- basic_memory/mcp/tools/canvas.py +34 -12
- basic_memory/mcp/tools/chatgpt_tools.py +4 -1
- basic_memory/mcp/tools/delete_note.py +31 -7
- basic_memory/mcp/tools/edit_note.py +14 -9
- basic_memory/mcp/tools/list_directory.py +7 -17
- basic_memory/mcp/tools/move_note.py +35 -31
- basic_memory/mcp/tools/project_management.py +29 -25
- basic_memory/mcp/tools/read_content.py +13 -3
- basic_memory/mcp/tools/read_note.py +24 -14
- basic_memory/mcp/tools/recent_activity.py +32 -38
- basic_memory/mcp/tools/search.py +17 -10
- basic_memory/mcp/tools/utils.py +28 -0
- basic_memory/mcp/tools/view_note.py +2 -1
- basic_memory/mcp/tools/write_note.py +37 -14
- basic_memory/models/knowledge.py +15 -2
- basic_memory/models/project.py +7 -1
- basic_memory/models/search.py +58 -2
- basic_memory/project_resolver.py +222 -0
- basic_memory/repository/entity_repository.py +210 -3
- basic_memory/repository/observation_repository.py +1 -0
- basic_memory/repository/postgres_search_repository.py +451 -0
- basic_memory/repository/project_repository.py +38 -1
- basic_memory/repository/relation_repository.py +58 -2
- basic_memory/repository/repository.py +1 -0
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +77 -615
- basic_memory/repository/search_repository_base.py +241 -0
- basic_memory/repository/sqlite_search_repository.py +437 -0
- basic_memory/runtime.py +61 -0
- basic_memory/schemas/base.py +36 -6
- basic_memory/schemas/directory.py +2 -1
- basic_memory/schemas/memory.py +9 -2
- basic_memory/schemas/project_info.py +2 -0
- basic_memory/schemas/response.py +84 -27
- basic_memory/schemas/search.py +5 -0
- basic_memory/schemas/sync_report.py +1 -1
- basic_memory/schemas/v2/__init__.py +27 -0
- basic_memory/schemas/v2/entity.py +133 -0
- basic_memory/schemas/v2/resource.py +47 -0
- basic_memory/services/context_service.py +219 -43
- basic_memory/services/directory_service.py +26 -11
- basic_memory/services/entity_service.py +68 -33
- basic_memory/services/file_service.py +131 -16
- basic_memory/services/initialization.py +51 -26
- basic_memory/services/link_resolver.py +1 -0
- basic_memory/services/project_service.py +68 -43
- basic_memory/services/search_service.py +75 -16
- basic_memory/sync/__init__.py +2 -1
- basic_memory/sync/coordinator.py +160 -0
- basic_memory/sync/sync_service.py +135 -115
- basic_memory/sync/watch_service.py +32 -12
- basic_memory/telemetry.py +249 -0
- basic_memory/utils.py +96 -75
- {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/METADATA +129 -5
- basic_memory-0.17.4.dist-info/RECORD +193 -0
- {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
- basic_memory-0.16.1.dist-info/RECORD +0 -148
- {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +0 -0
- {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,
|
|
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"))
|
|
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=
|
|
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
|
-
#
|
|
196
|
-
if
|
|
197
|
-
|
|
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
|
-
)
|
|
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
|
-
|
|
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
|
-
@
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
458
|
-
|
|
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
|
-
|
|
496
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
516
|
-
|
|
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)
|