basic-memory 0.12.3__py3-none-any.whl → 0.13.0b2__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 +7 -1
- basic_memory/alembic/env.py +1 -1
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -5
- basic_memory/api/app.py +43 -13
- basic_memory/api/routers/__init__.py +4 -2
- basic_memory/api/routers/directory_router.py +63 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +127 -38
- basic_memory/api/routers/management_router.py +78 -0
- basic_memory/api/routers/memory_router.py +4 -59
- basic_memory/api/routers/project_router.py +230 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/search_router.py +3 -21
- basic_memory/api/routers/utils.py +130 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/cli/app.py +20 -21
- basic_memory/cli/commands/__init__.py +2 -1
- basic_memory/cli/commands/auth.py +136 -0
- basic_memory/cli/commands/db.py +3 -3
- basic_memory/cli/commands/import_chatgpt.py +31 -207
- basic_memory/cli/commands/import_claude_conversations.py +16 -142
- basic_memory/cli/commands/import_claude_projects.py +33 -143
- basic_memory/cli/commands/import_memory_json.py +26 -83
- basic_memory/cli/commands/mcp.py +71 -18
- basic_memory/cli/commands/project.py +99 -67
- basic_memory/cli/commands/status.py +19 -9
- basic_memory/cli/commands/sync.py +44 -58
- basic_memory/cli/main.py +1 -5
- basic_memory/config.py +144 -88
- basic_memory/db.py +6 -4
- basic_memory/deps.py +227 -30
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +222 -0
- basic_memory/importers/claude_conversations_importer.py +172 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +93 -0
- basic_memory/importers/utils.py +58 -0
- basic_memory/markdown/entity_parser.py +5 -2
- basic_memory/mcp/auth_provider.py +270 -0
- basic_memory/mcp/external_auth_provider.py +321 -0
- basic_memory/mcp/project_session.py +103 -0
- basic_memory/mcp/prompts/continue_conversation.py +18 -68
- basic_memory/mcp/prompts/recent_activity.py +19 -3
- basic_memory/mcp/prompts/search.py +14 -140
- basic_memory/mcp/prompts/utils.py +3 -3
- basic_memory/mcp/{tools → resources}/project_info.py +6 -2
- basic_memory/mcp/server.py +82 -8
- basic_memory/mcp/supabase_auth_provider.py +463 -0
- basic_memory/mcp/tools/__init__.py +20 -0
- basic_memory/mcp/tools/build_context.py +11 -1
- basic_memory/mcp/tools/canvas.py +15 -2
- basic_memory/mcp/tools/delete_note.py +12 -4
- basic_memory/mcp/tools/edit_note.py +297 -0
- basic_memory/mcp/tools/list_directory.py +154 -0
- basic_memory/mcp/tools/move_note.py +87 -0
- basic_memory/mcp/tools/project_management.py +300 -0
- basic_memory/mcp/tools/read_content.py +15 -6
- basic_memory/mcp/tools/read_note.py +17 -5
- basic_memory/mcp/tools/recent_activity.py +11 -2
- basic_memory/mcp/tools/search.py +10 -1
- basic_memory/mcp/tools/utils.py +137 -12
- basic_memory/mcp/tools/write_note.py +11 -15
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +16 -4
- basic_memory/models/project.py +80 -0
- basic_memory/models/search.py +8 -5
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +8 -3
- basic_memory/repository/observation_repository.py +35 -3
- basic_memory/repository/project_info_repository.py +3 -2
- basic_memory/repository/project_repository.py +85 -0
- basic_memory/repository/relation_repository.py +8 -2
- basic_memory/repository/repository.py +107 -15
- basic_memory/repository/search_repository.py +87 -27
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +34 -0
- basic_memory/schemas/memory.py +26 -12
- basic_memory/schemas/project_info.py +112 -2
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +56 -2
- basic_memory/schemas/search.py +1 -1
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +208 -95
- basic_memory/services/directory_service.py +167 -0
- basic_memory/services/entity_service.py +385 -5
- basic_memory/services/exceptions.py +6 -0
- basic_memory/services/file_service.py +14 -15
- basic_memory/services/initialization.py +144 -67
- basic_memory/services/link_resolver.py +16 -8
- basic_memory/services/project_service.py +548 -0
- basic_memory/services/search_service.py +77 -2
- basic_memory/sync/background_sync.py +25 -0
- basic_memory/sync/sync_service.py +10 -9
- basic_memory/sync/watch_service.py +63 -39
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/METADATA +23 -1
- basic_memory-0.13.0b2.dist-info/RECORD +132 -0
- basic_memory/api/routers/project_info_router.py +0 -274
- basic_memory/mcp/main.py +0 -24
- basic_memory-0.12.3.dist-info/RECORD +0 -100
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/WHEEL +0 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/licenses/LICENSE +0 -0
|
@@ -16,10 +16,12 @@ from basic_memory.cli.app import app
|
|
|
16
16
|
from basic_memory.config import config
|
|
17
17
|
from basic_memory.markdown import EntityParser
|
|
18
18
|
from basic_memory.markdown.markdown_processor import MarkdownProcessor
|
|
19
|
+
from basic_memory.models import Project
|
|
19
20
|
from basic_memory.repository import (
|
|
20
21
|
EntityRepository,
|
|
21
22
|
ObservationRepository,
|
|
22
23
|
RelationRepository,
|
|
24
|
+
ProjectRepository,
|
|
23
25
|
)
|
|
24
26
|
from basic_memory.repository.search_repository import SearchRepository
|
|
25
27
|
from basic_memory.services import EntityService, FileService
|
|
@@ -27,7 +29,7 @@ from basic_memory.services.link_resolver import LinkResolver
|
|
|
27
29
|
from basic_memory.services.search_service import SearchService
|
|
28
30
|
from basic_memory.sync import SyncService
|
|
29
31
|
from basic_memory.sync.sync_service import SyncReport
|
|
30
|
-
from basic_memory.
|
|
32
|
+
from basic_memory.config import app_config
|
|
31
33
|
|
|
32
34
|
console = Console()
|
|
33
35
|
|
|
@@ -38,21 +40,22 @@ class ValidationIssue:
|
|
|
38
40
|
error: str
|
|
39
41
|
|
|
40
42
|
|
|
41
|
-
async def get_sync_service(): # pragma: no cover
|
|
43
|
+
async def get_sync_service(project: Project) -> SyncService: # pragma: no cover
|
|
42
44
|
"""Get sync service instance with all dependencies."""
|
|
43
45
|
_, session_maker = await db.get_or_create_db(
|
|
44
|
-
db_path=
|
|
46
|
+
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
|
45
47
|
)
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
project_path = Path(project.path)
|
|
50
|
+
entity_parser = EntityParser(project_path)
|
|
48
51
|
markdown_processor = MarkdownProcessor(entity_parser)
|
|
49
|
-
file_service = FileService(
|
|
52
|
+
file_service = FileService(project_path, markdown_processor)
|
|
50
53
|
|
|
51
54
|
# Initialize repositories
|
|
52
|
-
entity_repository = EntityRepository(session_maker)
|
|
53
|
-
observation_repository = ObservationRepository(session_maker)
|
|
54
|
-
relation_repository = RelationRepository(session_maker)
|
|
55
|
-
search_repository = SearchRepository(session_maker)
|
|
55
|
+
entity_repository = EntityRepository(session_maker, project_id=project.id)
|
|
56
|
+
observation_repository = ObservationRepository(session_maker, project_id=project.id)
|
|
57
|
+
relation_repository = RelationRepository(session_maker, project_id=project.id)
|
|
58
|
+
search_repository = SearchRepository(session_maker, project_id=project.id)
|
|
56
59
|
|
|
57
60
|
# Initialize services
|
|
58
61
|
search_service = SearchService(search_repository, entity_repository, file_service)
|
|
@@ -70,7 +73,7 @@ async def get_sync_service(): # pragma: no cover
|
|
|
70
73
|
|
|
71
74
|
# Create sync service
|
|
72
75
|
sync_service = SyncService(
|
|
73
|
-
|
|
76
|
+
app_config=app_config,
|
|
74
77
|
entity_service=entity_service,
|
|
75
78
|
entity_parser=entity_parser,
|
|
76
79
|
entity_repository=entity_repository,
|
|
@@ -153,8 +156,16 @@ def display_detailed_sync_results(knowledge: SyncReport):
|
|
|
153
156
|
console.print(knowledge_tree)
|
|
154
157
|
|
|
155
158
|
|
|
156
|
-
async def run_sync(verbose: bool = False
|
|
159
|
+
async def run_sync(verbose: bool = False):
|
|
157
160
|
"""Run sync operation."""
|
|
161
|
+
_, session_maker = await db.get_or_create_db(
|
|
162
|
+
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
|
163
|
+
)
|
|
164
|
+
project_repository = ProjectRepository(session_maker)
|
|
165
|
+
project = await project_repository.get_by_name(config.project)
|
|
166
|
+
if not project: # pragma: no cover
|
|
167
|
+
raise Exception(f"Project '{config.project}' not found")
|
|
168
|
+
|
|
158
169
|
import time
|
|
159
170
|
|
|
160
171
|
start_time = time.time()
|
|
@@ -162,50 +173,33 @@ async def run_sync(verbose: bool = False, watch: bool = False, console_status: b
|
|
|
162
173
|
logger.info(
|
|
163
174
|
"Sync command started",
|
|
164
175
|
project=config.project,
|
|
165
|
-
watch_mode=watch,
|
|
166
176
|
verbose=verbose,
|
|
167
177
|
directory=str(config.home),
|
|
168
178
|
)
|
|
169
179
|
|
|
170
|
-
sync_service = await get_sync_service()
|
|
180
|
+
sync_service = await get_sync_service(project)
|
|
171
181
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
logger.info("Starting watch service after initial sync")
|
|
175
|
-
watch_service = WatchService(
|
|
176
|
-
sync_service=sync_service,
|
|
177
|
-
file_service=sync_service.entity_service.file_service,
|
|
178
|
-
config=config,
|
|
179
|
-
)
|
|
182
|
+
logger.info("Running one-time sync")
|
|
183
|
+
knowledge_changes = await sync_service.sync(config.home)
|
|
180
184
|
|
|
181
|
-
|
|
182
|
-
|
|
185
|
+
# Log results
|
|
186
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
187
|
+
logger.info(
|
|
188
|
+
"Sync command completed",
|
|
189
|
+
project=config.project,
|
|
190
|
+
total_changes=knowledge_changes.total,
|
|
191
|
+
new_files=len(knowledge_changes.new),
|
|
192
|
+
modified_files=len(knowledge_changes.modified),
|
|
193
|
+
deleted_files=len(knowledge_changes.deleted),
|
|
194
|
+
moved_files=len(knowledge_changes.moves),
|
|
195
|
+
duration_ms=duration_ms,
|
|
196
|
+
)
|
|
183
197
|
|
|
184
|
-
|
|
185
|
-
|
|
198
|
+
# Display results
|
|
199
|
+
if verbose:
|
|
200
|
+
display_detailed_sync_results(knowledge_changes)
|
|
186
201
|
else:
|
|
187
|
-
#
|
|
188
|
-
logger.info("Running one-time sync")
|
|
189
|
-
knowledge_changes = await sync_service.sync(config.home)
|
|
190
|
-
|
|
191
|
-
# Log results
|
|
192
|
-
duration_ms = int((time.time() - start_time) * 1000)
|
|
193
|
-
logger.info(
|
|
194
|
-
"Sync command completed",
|
|
195
|
-
project=config.project,
|
|
196
|
-
total_changes=knowledge_changes.total,
|
|
197
|
-
new_files=len(knowledge_changes.new),
|
|
198
|
-
modified_files=len(knowledge_changes.modified),
|
|
199
|
-
deleted_files=len(knowledge_changes.deleted),
|
|
200
|
-
moved_files=len(knowledge_changes.moves),
|
|
201
|
-
duration_ms=duration_ms,
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
# Display results
|
|
205
|
-
if verbose:
|
|
206
|
-
display_detailed_sync_results(knowledge_changes)
|
|
207
|
-
else:
|
|
208
|
-
display_sync_summary(knowledge_changes) # pragma: no cover
|
|
202
|
+
display_sync_summary(knowledge_changes) # pragma: no cover
|
|
209
203
|
|
|
210
204
|
|
|
211
205
|
@app.command()
|
|
@@ -216,22 +210,15 @@ def sync(
|
|
|
216
210
|
"-v",
|
|
217
211
|
help="Show detailed sync information.",
|
|
218
212
|
),
|
|
219
|
-
watch: bool = typer.Option(
|
|
220
|
-
False,
|
|
221
|
-
"--watch",
|
|
222
|
-
"-w",
|
|
223
|
-
help="Start watching for changes after sync.",
|
|
224
|
-
),
|
|
225
213
|
) -> None:
|
|
226
214
|
"""Sync knowledge files with the database."""
|
|
227
215
|
try:
|
|
228
216
|
# Show which project we're syncing
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
typer.echo(f"Project path: {config.home}")
|
|
217
|
+
typer.echo(f"Syncing project: {config.project}")
|
|
218
|
+
typer.echo(f"Project path: {config.home}")
|
|
232
219
|
|
|
233
220
|
# Run sync
|
|
234
|
-
asyncio.run(run_sync(verbose=verbose
|
|
221
|
+
asyncio.run(run_sync(verbose=verbose))
|
|
235
222
|
|
|
236
223
|
except Exception as e: # pragma: no cover
|
|
237
224
|
if not isinstance(e, typer.Exit):
|
|
@@ -240,7 +227,6 @@ def sync(
|
|
|
240
227
|
f"project={config.project},"
|
|
241
228
|
f"error={str(e)},"
|
|
242
229
|
f"error_type={type(e).__name__},"
|
|
243
|
-
f"watch_mode={watch},"
|
|
244
230
|
f"directory={str(config.home)}",
|
|
245
231
|
)
|
|
246
232
|
typer.echo(f"Error during sync: {e}", err=True)
|
basic_memory/cli/main.py
CHANGED
|
@@ -4,6 +4,7 @@ from basic_memory.cli.app import app # pragma: no cover
|
|
|
4
4
|
|
|
5
5
|
# Register commands
|
|
6
6
|
from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
|
|
7
|
+
auth,
|
|
7
8
|
db,
|
|
8
9
|
import_chatgpt,
|
|
9
10
|
import_claude_conversations,
|
|
@@ -15,12 +16,7 @@ from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
|
|
|
15
16
|
sync,
|
|
16
17
|
tool,
|
|
17
18
|
)
|
|
18
|
-
from basic_memory.config import config
|
|
19
|
-
from basic_memory.services.initialization import ensure_initialization
|
|
20
19
|
|
|
21
20
|
if __name__ == "__main__": # pragma: no cover
|
|
22
|
-
# Run initialization if we are starting as a module
|
|
23
|
-
ensure_initialization(config)
|
|
24
|
-
|
|
25
21
|
# start the app
|
|
26
22
|
app()
|
basic_memory/config.py
CHANGED
|
@@ -2,76 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
from typing import Any, Dict, Literal, Optional
|
|
7
|
+
from typing import Any, Dict, Literal, Optional, List
|
|
7
8
|
|
|
8
9
|
from loguru import logger
|
|
9
10
|
from pydantic import Field, field_validator
|
|
10
11
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
11
12
|
|
|
12
13
|
import basic_memory
|
|
13
|
-
from basic_memory.utils import setup_logging
|
|
14
|
+
from basic_memory.utils import setup_logging, generate_permalink
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
DATABASE_NAME = "memory.db"
|
|
18
|
+
APP_DATABASE_NAME = "memory.db" # Using the same name but in the app directory
|
|
16
19
|
DATA_DIR_NAME = ".basic-memory"
|
|
17
20
|
CONFIG_FILE_NAME = "config.json"
|
|
21
|
+
WATCH_STATUS_JSON = "watch-status.json"
|
|
18
22
|
|
|
19
23
|
Environment = Literal["test", "dev", "user"]
|
|
20
24
|
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
@dataclass
|
|
27
|
+
class ProjectConfig:
|
|
23
28
|
"""Configuration for a specific basic-memory project."""
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# Default to ~/basic-memory but allow override with env var: BASIC_MEMORY_HOME
|
|
28
|
-
home: Path = Field(
|
|
29
|
-
default_factory=lambda: Path.home() / "basic-memory",
|
|
30
|
-
description="Base path for basic-memory files",
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
# Name of the project
|
|
34
|
-
project: str = Field(default="default", description="Project name")
|
|
35
|
-
|
|
36
|
-
# Watch service configuration
|
|
37
|
-
sync_delay: int = Field(
|
|
38
|
-
default=1000, description="Milliseconds to wait after changes before syncing", gt=0
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
# update permalinks on move
|
|
42
|
-
update_permalinks_on_move: bool = Field(
|
|
43
|
-
default=False,
|
|
44
|
-
description="Whether to update permalinks when files are moved or renamed. default (False)",
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
model_config = SettingsConfigDict(
|
|
48
|
-
env_prefix="BASIC_MEMORY_",
|
|
49
|
-
extra="ignore",
|
|
50
|
-
env_file=".env",
|
|
51
|
-
env_file_encoding="utf-8",
|
|
52
|
-
)
|
|
30
|
+
name: str
|
|
31
|
+
home: Path
|
|
53
32
|
|
|
54
33
|
@property
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
database_path = self.home / DATA_DIR_NAME / DATABASE_NAME
|
|
58
|
-
if not database_path.exists():
|
|
59
|
-
database_path.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
-
database_path.touch()
|
|
61
|
-
return database_path
|
|
34
|
+
def project(self):
|
|
35
|
+
return self.name
|
|
62
36
|
|
|
63
|
-
@
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"""Ensure project path exists."""
|
|
67
|
-
if not v.exists():
|
|
68
|
-
v.mkdir(parents=True)
|
|
69
|
-
return v
|
|
37
|
+
@property
|
|
38
|
+
def project_url(self) -> str: # pragma: no cover
|
|
39
|
+
return f"/{generate_permalink(self.name)}"
|
|
70
40
|
|
|
71
41
|
|
|
72
42
|
class BasicMemoryConfig(BaseSettings):
|
|
73
43
|
"""Pydantic model for Basic Memory global configuration."""
|
|
74
44
|
|
|
45
|
+
env: Environment = Field(default="dev", description="Environment name")
|
|
46
|
+
|
|
75
47
|
projects: Dict[str, str] = Field(
|
|
76
48
|
default_factory=lambda: {"main": str(Path.home() / "basic-memory")},
|
|
77
49
|
description="Mapping of project names to their filesystem paths",
|
|
@@ -81,8 +53,15 @@ class BasicMemoryConfig(BaseSettings):
|
|
|
81
53
|
description="Name of the default project to use",
|
|
82
54
|
)
|
|
83
55
|
|
|
56
|
+
# overridden by ~/.basic-memory/config.json
|
|
84
57
|
log_level: str = "INFO"
|
|
85
58
|
|
|
59
|
+
# Watch service configuration
|
|
60
|
+
sync_delay: int = Field(
|
|
61
|
+
default=1000, description="Milliseconds to wait after changes before syncing", gt=0
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# update permalinks on move
|
|
86
65
|
update_permalinks_on_move: bool = Field(
|
|
87
66
|
default=False,
|
|
88
67
|
description="Whether to update permalinks when files are moved or renamed. default (False)",
|
|
@@ -96,25 +75,84 @@ class BasicMemoryConfig(BaseSettings):
|
|
|
96
75
|
model_config = SettingsConfigDict(
|
|
97
76
|
env_prefix="BASIC_MEMORY_",
|
|
98
77
|
extra="ignore",
|
|
78
|
+
env_file=".env",
|
|
79
|
+
env_file_encoding="utf-8",
|
|
99
80
|
)
|
|
100
81
|
|
|
82
|
+
def get_project_path(self, project_name: Optional[str] = None) -> Path: # pragma: no cover
|
|
83
|
+
"""Get the path for a specific project or the default project."""
|
|
84
|
+
name = project_name or self.default_project
|
|
85
|
+
|
|
86
|
+
if name not in self.projects:
|
|
87
|
+
raise ValueError(f"Project '{name}' not found in configuration")
|
|
88
|
+
|
|
89
|
+
return Path(self.projects[name])
|
|
90
|
+
|
|
101
91
|
def model_post_init(self, __context: Any) -> None:
|
|
102
92
|
"""Ensure configuration is valid after initialization."""
|
|
103
93
|
# Ensure main project exists
|
|
104
|
-
if "main" not in self.projects:
|
|
94
|
+
if "main" not in self.projects: # pragma: no cover
|
|
105
95
|
self.projects["main"] = str(Path.home() / "basic-memory")
|
|
106
96
|
|
|
107
97
|
# Ensure default project is valid
|
|
108
|
-
if self.default_project not in self.projects:
|
|
98
|
+
if self.default_project not in self.projects: # pragma: no cover
|
|
109
99
|
self.default_project = "main"
|
|
110
100
|
|
|
101
|
+
@property
|
|
102
|
+
def app_database_path(self) -> Path:
|
|
103
|
+
"""Get the path to the app-level database.
|
|
104
|
+
|
|
105
|
+
This is the single database that will store all knowledge data
|
|
106
|
+
across all projects.
|
|
107
|
+
"""
|
|
108
|
+
database_path = Path.home() / DATA_DIR_NAME / APP_DATABASE_NAME
|
|
109
|
+
if not database_path.exists(): # pragma: no cover
|
|
110
|
+
database_path.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
database_path.touch()
|
|
112
|
+
return database_path
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def database_path(self) -> Path:
|
|
116
|
+
"""Get SQLite database path.
|
|
117
|
+
|
|
118
|
+
Rreturns the app-level database path
|
|
119
|
+
for backward compatibility in the codebase.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
# Load the app-level database path from the global config
|
|
123
|
+
config = config_manager.load_config() # pragma: no cover
|
|
124
|
+
return config.app_database_path # pragma: no cover
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def project_list(self) -> List[ProjectConfig]: # pragma: no cover
|
|
128
|
+
"""Get all configured projects as ProjectConfig objects."""
|
|
129
|
+
return [ProjectConfig(name=name, home=Path(path)) for name, path in self.projects.items()]
|
|
130
|
+
|
|
131
|
+
@field_validator("projects")
|
|
132
|
+
@classmethod
|
|
133
|
+
def ensure_project_paths_exists(cls, v: Dict[str, str]) -> Dict[str, str]: # pragma: no cover
|
|
134
|
+
"""Ensure project path exists."""
|
|
135
|
+
for name, path_value in v.items():
|
|
136
|
+
path = Path(path_value)
|
|
137
|
+
if not Path(path).exists():
|
|
138
|
+
try:
|
|
139
|
+
path.mkdir(parents=True)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"Failed to create project path: {e}")
|
|
142
|
+
raise e
|
|
143
|
+
return v
|
|
144
|
+
|
|
111
145
|
|
|
112
146
|
class ConfigManager:
|
|
113
147
|
"""Manages Basic Memory configuration."""
|
|
114
148
|
|
|
115
149
|
def __init__(self) -> None:
|
|
116
150
|
"""Initialize the configuration manager."""
|
|
117
|
-
|
|
151
|
+
home = os.getenv("HOME", Path.home())
|
|
152
|
+
if isinstance(home, str):
|
|
153
|
+
home = Path(home)
|
|
154
|
+
|
|
155
|
+
self.config_dir = home / DATA_DIR_NAME
|
|
118
156
|
self.config_file = self.config_dir / CONFIG_FILE_NAME
|
|
119
157
|
|
|
120
158
|
# Ensure config directory exists
|
|
@@ -129,7 +167,7 @@ class ConfigManager:
|
|
|
129
167
|
try:
|
|
130
168
|
data = json.loads(self.config_file.read_text(encoding="utf-8"))
|
|
131
169
|
return BasicMemoryConfig(**data)
|
|
132
|
-
except Exception as e:
|
|
170
|
+
except Exception as e: # pragma: no cover
|
|
133
171
|
logger.error(f"Failed to load config: {e}")
|
|
134
172
|
config = BasicMemoryConfig()
|
|
135
173
|
self.save_config(config)
|
|
@@ -141,7 +179,7 @@ class ConfigManager:
|
|
|
141
179
|
|
|
142
180
|
def save_config(self, config: BasicMemoryConfig) -> None:
|
|
143
181
|
"""Save configuration to file."""
|
|
144
|
-
try:
|
|
182
|
+
try:
|
|
145
183
|
self.config_file.write_text(json.dumps(config.model_dump(), indent=2))
|
|
146
184
|
except Exception as e: # pragma: no cover
|
|
147
185
|
logger.error(f"Failed to save config: {e}")
|
|
@@ -156,37 +194,25 @@ class ConfigManager:
|
|
|
156
194
|
"""Get the default project name."""
|
|
157
195
|
return self.config.default_project
|
|
158
196
|
|
|
159
|
-
def
|
|
160
|
-
"""Get the path for a specific project or the default project."""
|
|
161
|
-
name = project_name or self.config.default_project
|
|
162
|
-
|
|
163
|
-
# Check if specified in environment variable
|
|
164
|
-
if not project_name and "BASIC_MEMORY_PROJECT" in os.environ:
|
|
165
|
-
name = os.environ["BASIC_MEMORY_PROJECT"]
|
|
166
|
-
|
|
167
|
-
if name not in self.config.projects:
|
|
168
|
-
raise ValueError(f"Project '{name}' not found in configuration")
|
|
169
|
-
|
|
170
|
-
return Path(self.config.projects[name])
|
|
171
|
-
|
|
172
|
-
def add_project(self, name: str, path: str) -> None:
|
|
197
|
+
def add_project(self, name: str, path: str) -> ProjectConfig:
|
|
173
198
|
"""Add a new project to the configuration."""
|
|
174
|
-
if name in self.config.projects:
|
|
199
|
+
if name in self.config.projects: # pragma: no cover
|
|
175
200
|
raise ValueError(f"Project '{name}' already exists")
|
|
176
201
|
|
|
177
202
|
# Ensure the path exists
|
|
178
203
|
project_path = Path(path)
|
|
179
|
-
project_path.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
project_path.mkdir(parents=True, exist_ok=True) # pragma: no cover
|
|
180
205
|
|
|
181
206
|
self.config.projects[name] = str(project_path)
|
|
182
207
|
self.save_config(self.config)
|
|
208
|
+
return ProjectConfig(name=name, home=project_path)
|
|
183
209
|
|
|
184
210
|
def remove_project(self, name: str) -> None:
|
|
185
211
|
"""Remove a project from the configuration."""
|
|
186
|
-
if name not in self.config.projects:
|
|
212
|
+
if name not in self.config.projects: # pragma: no cover
|
|
187
213
|
raise ValueError(f"Project '{name}' not found")
|
|
188
214
|
|
|
189
|
-
if name == self.config.default_project:
|
|
215
|
+
if name == self.config.default_project: # pragma: no cover
|
|
190
216
|
raise ValueError(f"Cannot remove the default project '{name}'")
|
|
191
217
|
|
|
192
218
|
del self.config.projects[name]
|
|
@@ -202,33 +228,59 @@ class ConfigManager:
|
|
|
202
228
|
|
|
203
229
|
|
|
204
230
|
def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
|
|
205
|
-
"""
|
|
206
|
-
|
|
231
|
+
"""
|
|
232
|
+
Get the project configuration for the current session.
|
|
233
|
+
If project_name is provided, it will be used instead of the default project.
|
|
234
|
+
"""
|
|
207
235
|
|
|
208
|
-
|
|
209
|
-
actual_project_name = os.environ.get(
|
|
210
|
-
"BASIC_MEMORY_PROJECT", project_name or config_manager.default_project
|
|
211
|
-
)
|
|
236
|
+
actual_project_name = None
|
|
212
237
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
238
|
+
# load the config from file
|
|
239
|
+
global app_config
|
|
240
|
+
app_config = config_manager.load_config()
|
|
241
|
+
|
|
242
|
+
# Get project name from environment variable
|
|
243
|
+
os_project_name = os.environ.get("BASIC_MEMORY_PROJECT", None)
|
|
244
|
+
if os_project_name: # pragma: no cover
|
|
245
|
+
logger.warning(
|
|
246
|
+
f"BASIC_MEMORY_PROJECT is not supported anymore. Use the --project flag or set the default project in the config instead. Setting default project to {os_project_name}"
|
|
220
247
|
)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
248
|
+
actual_project_name = project_name
|
|
249
|
+
# if the project_name is passed in, use it
|
|
250
|
+
elif not project_name:
|
|
251
|
+
# use default
|
|
252
|
+
actual_project_name = app_config.default_project
|
|
253
|
+
else: # pragma: no cover
|
|
254
|
+
actual_project_name = project_name
|
|
255
|
+
|
|
256
|
+
# the config contains a dict[str,str] of project names and absolute paths
|
|
257
|
+
assert actual_project_name is not None, "actual_project_name cannot be None"
|
|
258
|
+
|
|
259
|
+
project_path = app_config.projects.get(actual_project_name)
|
|
260
|
+
if not project_path: # pragma: no cover
|
|
261
|
+
raise ValueError(f"Project '{actual_project_name}' not found")
|
|
262
|
+
|
|
263
|
+
return ProjectConfig(name=actual_project_name, home=Path(project_path))
|
|
225
264
|
|
|
226
265
|
|
|
227
266
|
# Create config manager
|
|
228
267
|
config_manager = ConfigManager()
|
|
229
268
|
|
|
230
|
-
#
|
|
231
|
-
|
|
269
|
+
# Export the app-level config
|
|
270
|
+
app_config: BasicMemoryConfig = config_manager.config
|
|
271
|
+
|
|
272
|
+
# Load project config for the default project (backward compatibility)
|
|
273
|
+
config: ProjectConfig = get_project_config()
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def update_current_project(project_name: str) -> None:
|
|
277
|
+
"""Update the global config to use a different project.
|
|
278
|
+
|
|
279
|
+
This is used by the CLI when --project flag is specified.
|
|
280
|
+
"""
|
|
281
|
+
global config
|
|
282
|
+
config = get_project_config(project_name) # pragma: no cover
|
|
283
|
+
|
|
232
284
|
|
|
233
285
|
# setup logging to a single log file in user home directory
|
|
234
286
|
user_home = Path.home()
|
|
@@ -236,6 +288,7 @@ log_dir = user_home / DATA_DIR_NAME
|
|
|
236
288
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
237
289
|
|
|
238
290
|
|
|
291
|
+
# Process info for logging
|
|
239
292
|
def get_process_name(): # pragma: no cover
|
|
240
293
|
"""
|
|
241
294
|
get the type of process for logging
|
|
@@ -258,6 +311,9 @@ process_name = get_process_name()
|
|
|
258
311
|
_LOGGING_SETUP = False
|
|
259
312
|
|
|
260
313
|
|
|
314
|
+
# Logging
|
|
315
|
+
|
|
316
|
+
|
|
261
317
|
def setup_basic_memory_logging(): # pragma: no cover
|
|
262
318
|
"""Set up logging for basic-memory, ensuring it only happens once."""
|
|
263
319
|
global _LOGGING_SETUP
|
|
@@ -267,7 +323,7 @@ def setup_basic_memory_logging(): # pragma: no cover
|
|
|
267
323
|
return
|
|
268
324
|
|
|
269
325
|
setup_logging(
|
|
270
|
-
env=config.env,
|
|
326
|
+
env=config_manager.config.env,
|
|
271
327
|
home_dir=user_home, # Use user home for logs
|
|
272
328
|
log_level=config_manager.load_config().log_level,
|
|
273
329
|
log_file=f"{DATA_DIR_NAME}/basic-memory-{process_name}.log",
|
basic_memory/db.py
CHANGED
|
@@ -4,8 +4,7 @@ from enum import Enum, auto
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import AsyncGenerator, Optional
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
from basic_memory.config import ProjectConfig
|
|
7
|
+
from basic_memory.config import BasicMemoryConfig
|
|
9
8
|
from alembic import command
|
|
10
9
|
from alembic.config import Config
|
|
11
10
|
|
|
@@ -147,7 +146,7 @@ async def engine_session_factory(
|
|
|
147
146
|
|
|
148
147
|
|
|
149
148
|
async def run_migrations(
|
|
150
|
-
app_config:
|
|
149
|
+
app_config: BasicMemoryConfig, database_type=DatabaseType.FILESYSTEM
|
|
151
150
|
): # pragma: no cover
|
|
152
151
|
"""Run any pending alembic migrations."""
|
|
153
152
|
logger.info("Running database migrations...")
|
|
@@ -172,7 +171,10 @@ async def run_migrations(
|
|
|
172
171
|
logger.info("Migrations completed successfully")
|
|
173
172
|
|
|
174
173
|
_, session_maker = await get_or_create_db(app_config.database_path, database_type)
|
|
175
|
-
|
|
174
|
+
|
|
175
|
+
# initialize the search Index schema
|
|
176
|
+
# the project_id is not used for init_search_index, so we pass a dummy value
|
|
177
|
+
await SearchRepository(session_maker, 1).init_search_index()
|
|
176
178
|
except Exception as e: # pragma: no cover
|
|
177
179
|
logger.error(f"Error running migrations: {e}")
|
|
178
180
|
raise
|