basic-memory 0.12.3__py3-none-any.whl → 0.13.0b1__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 +145 -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.0b1.dist-info}/METADATA +23 -1
- basic_memory-0.13.0b1.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.0b1.dist-info}/WHEEL +0 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.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,49 @@
|
|
|
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
|
|
12
|
+
from setuptools.command.setopt import config_file
|
|
11
13
|
|
|
12
14
|
import basic_memory
|
|
13
|
-
from basic_memory.utils import setup_logging
|
|
15
|
+
from basic_memory.utils import setup_logging, generate_permalink
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
DATABASE_NAME = "memory.db"
|
|
19
|
+
APP_DATABASE_NAME = "memory.db" # Using the same name but in the app directory
|
|
16
20
|
DATA_DIR_NAME = ".basic-memory"
|
|
17
21
|
CONFIG_FILE_NAME = "config.json"
|
|
22
|
+
WATCH_STATUS_JSON = "watch-status.json"
|
|
18
23
|
|
|
19
24
|
Environment = Literal["test", "dev", "user"]
|
|
20
25
|
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
@dataclass
|
|
28
|
+
class ProjectConfig:
|
|
23
29
|
"""Configuration for a specific basic-memory project."""
|
|
24
30
|
|
|
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
|
-
)
|
|
31
|
+
name: str
|
|
32
|
+
home: Path
|
|
53
33
|
|
|
54
34
|
@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
|
|
35
|
+
def project(self):
|
|
36
|
+
return self.name
|
|
62
37
|
|
|
63
|
-
@
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"""Ensure project path exists."""
|
|
67
|
-
if not v.exists():
|
|
68
|
-
v.mkdir(parents=True)
|
|
69
|
-
return v
|
|
38
|
+
@property
|
|
39
|
+
def project_url(self) -> str: # pragma: no cover
|
|
40
|
+
return f"/{generate_permalink(self.name)}"
|
|
70
41
|
|
|
71
42
|
|
|
72
43
|
class BasicMemoryConfig(BaseSettings):
|
|
73
44
|
"""Pydantic model for Basic Memory global configuration."""
|
|
74
45
|
|
|
46
|
+
env: Environment = Field(default="dev", description="Environment name")
|
|
47
|
+
|
|
75
48
|
projects: Dict[str, str] = Field(
|
|
76
49
|
default_factory=lambda: {"main": str(Path.home() / "basic-memory")},
|
|
77
50
|
description="Mapping of project names to their filesystem paths",
|
|
@@ -81,8 +54,15 @@ class BasicMemoryConfig(BaseSettings):
|
|
|
81
54
|
description="Name of the default project to use",
|
|
82
55
|
)
|
|
83
56
|
|
|
57
|
+
# overridden by ~/.basic-memory/config.json
|
|
84
58
|
log_level: str = "INFO"
|
|
85
59
|
|
|
60
|
+
# Watch service configuration
|
|
61
|
+
sync_delay: int = Field(
|
|
62
|
+
default=1000, description="Milliseconds to wait after changes before syncing", gt=0
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# update permalinks on move
|
|
86
66
|
update_permalinks_on_move: bool = Field(
|
|
87
67
|
default=False,
|
|
88
68
|
description="Whether to update permalinks when files are moved or renamed. default (False)",
|
|
@@ -96,25 +76,84 @@ class BasicMemoryConfig(BaseSettings):
|
|
|
96
76
|
model_config = SettingsConfigDict(
|
|
97
77
|
env_prefix="BASIC_MEMORY_",
|
|
98
78
|
extra="ignore",
|
|
79
|
+
env_file=".env",
|
|
80
|
+
env_file_encoding="utf-8",
|
|
99
81
|
)
|
|
100
82
|
|
|
83
|
+
def get_project_path(self, project_name: Optional[str] = None) -> Path: # pragma: no cover
|
|
84
|
+
"""Get the path for a specific project or the default project."""
|
|
85
|
+
name = project_name or self.default_project
|
|
86
|
+
|
|
87
|
+
if name not in self.projects:
|
|
88
|
+
raise ValueError(f"Project '{name}' not found in configuration")
|
|
89
|
+
|
|
90
|
+
return Path(self.projects[name])
|
|
91
|
+
|
|
101
92
|
def model_post_init(self, __context: Any) -> None:
|
|
102
93
|
"""Ensure configuration is valid after initialization."""
|
|
103
94
|
# Ensure main project exists
|
|
104
|
-
if "main" not in self.projects:
|
|
95
|
+
if "main" not in self.projects: # pragma: no cover
|
|
105
96
|
self.projects["main"] = str(Path.home() / "basic-memory")
|
|
106
97
|
|
|
107
98
|
# Ensure default project is valid
|
|
108
|
-
if self.default_project not in self.projects:
|
|
99
|
+
if self.default_project not in self.projects: # pragma: no cover
|
|
109
100
|
self.default_project = "main"
|
|
110
101
|
|
|
102
|
+
@property
|
|
103
|
+
def app_database_path(self) -> Path:
|
|
104
|
+
"""Get the path to the app-level database.
|
|
105
|
+
|
|
106
|
+
This is the single database that will store all knowledge data
|
|
107
|
+
across all projects.
|
|
108
|
+
"""
|
|
109
|
+
database_path = Path.home() / DATA_DIR_NAME / APP_DATABASE_NAME
|
|
110
|
+
if not database_path.exists(): # pragma: no cover
|
|
111
|
+
database_path.parent.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
database_path.touch()
|
|
113
|
+
return database_path
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def database_path(self) -> Path:
|
|
117
|
+
"""Get SQLite database path.
|
|
118
|
+
|
|
119
|
+
Rreturns the app-level database path
|
|
120
|
+
for backward compatibility in the codebase.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
# Load the app-level database path from the global config
|
|
124
|
+
config = config_manager.load_config() # pragma: no cover
|
|
125
|
+
return config.app_database_path # pragma: no cover
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def project_list(self) -> List[ProjectConfig]: # pragma: no cover
|
|
129
|
+
"""Get all configured projects as ProjectConfig objects."""
|
|
130
|
+
return [ProjectConfig(name=name, home=Path(path)) for name, path in self.projects.items()]
|
|
131
|
+
|
|
132
|
+
@field_validator("projects")
|
|
133
|
+
@classmethod
|
|
134
|
+
def ensure_project_paths_exists(cls, v: Dict[str, str]) -> Dict[str, str]: # pragma: no cover
|
|
135
|
+
"""Ensure project path exists."""
|
|
136
|
+
for name, path_value in v.items():
|
|
137
|
+
path = Path(path_value)
|
|
138
|
+
if not Path(path).exists():
|
|
139
|
+
try:
|
|
140
|
+
path.mkdir(parents=True)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error(f"Failed to create project path: {e}")
|
|
143
|
+
raise e
|
|
144
|
+
return v
|
|
145
|
+
|
|
111
146
|
|
|
112
147
|
class ConfigManager:
|
|
113
148
|
"""Manages Basic Memory configuration."""
|
|
114
149
|
|
|
115
150
|
def __init__(self) -> None:
|
|
116
151
|
"""Initialize the configuration manager."""
|
|
117
|
-
|
|
152
|
+
home = os.getenv("HOME", Path.home())
|
|
153
|
+
if isinstance(home, str):
|
|
154
|
+
home = Path(home)
|
|
155
|
+
|
|
156
|
+
self.config_dir = home / DATA_DIR_NAME
|
|
118
157
|
self.config_file = self.config_dir / CONFIG_FILE_NAME
|
|
119
158
|
|
|
120
159
|
# Ensure config directory exists
|
|
@@ -129,7 +168,7 @@ class ConfigManager:
|
|
|
129
168
|
try:
|
|
130
169
|
data = json.loads(self.config_file.read_text(encoding="utf-8"))
|
|
131
170
|
return BasicMemoryConfig(**data)
|
|
132
|
-
except Exception as e:
|
|
171
|
+
except Exception as e: # pragma: no cover
|
|
133
172
|
logger.error(f"Failed to load config: {e}")
|
|
134
173
|
config = BasicMemoryConfig()
|
|
135
174
|
self.save_config(config)
|
|
@@ -141,7 +180,7 @@ class ConfigManager:
|
|
|
141
180
|
|
|
142
181
|
def save_config(self, config: BasicMemoryConfig) -> None:
|
|
143
182
|
"""Save configuration to file."""
|
|
144
|
-
try:
|
|
183
|
+
try:
|
|
145
184
|
self.config_file.write_text(json.dumps(config.model_dump(), indent=2))
|
|
146
185
|
except Exception as e: # pragma: no cover
|
|
147
186
|
logger.error(f"Failed to save config: {e}")
|
|
@@ -156,37 +195,25 @@ class ConfigManager:
|
|
|
156
195
|
"""Get the default project name."""
|
|
157
196
|
return self.config.default_project
|
|
158
197
|
|
|
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:
|
|
198
|
+
def add_project(self, name: str, path: str) -> ProjectConfig:
|
|
173
199
|
"""Add a new project to the configuration."""
|
|
174
|
-
if name in self.config.projects:
|
|
200
|
+
if name in self.config.projects: # pragma: no cover
|
|
175
201
|
raise ValueError(f"Project '{name}' already exists")
|
|
176
202
|
|
|
177
203
|
# Ensure the path exists
|
|
178
204
|
project_path = Path(path)
|
|
179
|
-
project_path.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
project_path.mkdir(parents=True, exist_ok=True) # pragma: no cover
|
|
180
206
|
|
|
181
207
|
self.config.projects[name] = str(project_path)
|
|
182
208
|
self.save_config(self.config)
|
|
209
|
+
return ProjectConfig(name=name, home=project_path)
|
|
183
210
|
|
|
184
211
|
def remove_project(self, name: str) -> None:
|
|
185
212
|
"""Remove a project from the configuration."""
|
|
186
|
-
if name not in self.config.projects:
|
|
213
|
+
if name not in self.config.projects: # pragma: no cover
|
|
187
214
|
raise ValueError(f"Project '{name}' not found")
|
|
188
215
|
|
|
189
|
-
if name == self.config.default_project:
|
|
216
|
+
if name == self.config.default_project: # pragma: no cover
|
|
190
217
|
raise ValueError(f"Cannot remove the default project '{name}'")
|
|
191
218
|
|
|
192
219
|
del self.config.projects[name]
|
|
@@ -202,33 +229,59 @@ class ConfigManager:
|
|
|
202
229
|
|
|
203
230
|
|
|
204
231
|
def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
|
|
205
|
-
"""
|
|
206
|
-
|
|
232
|
+
"""
|
|
233
|
+
Get the project configuration for the current session.
|
|
234
|
+
If project_name is provided, it will be used instead of the default project.
|
|
235
|
+
"""
|
|
207
236
|
|
|
208
|
-
|
|
209
|
-
actual_project_name = os.environ.get(
|
|
210
|
-
"BASIC_MEMORY_PROJECT", project_name or config_manager.default_project
|
|
211
|
-
)
|
|
237
|
+
actual_project_name = None
|
|
212
238
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
239
|
+
# load the config from file
|
|
240
|
+
global app_config
|
|
241
|
+
app_config = config_manager.load_config()
|
|
242
|
+
|
|
243
|
+
# Get project name from environment variable
|
|
244
|
+
os_project_name = os.environ.get("BASIC_MEMORY_PROJECT", None)
|
|
245
|
+
if os_project_name: # pragma: no cover
|
|
246
|
+
logger.warning(
|
|
247
|
+
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
248
|
)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
249
|
+
actual_project_name = project_name
|
|
250
|
+
# if the project_name is passed in, use it
|
|
251
|
+
elif not project_name:
|
|
252
|
+
# use default
|
|
253
|
+
actual_project_name = app_config.default_project
|
|
254
|
+
else: # pragma: no cover
|
|
255
|
+
actual_project_name = project_name
|
|
256
|
+
|
|
257
|
+
# the config contains a dict[str,str] of project names and absolute paths
|
|
258
|
+
assert actual_project_name is not None, "actual_project_name cannot be None"
|
|
259
|
+
|
|
260
|
+
project_path = app_config.projects.get(actual_project_name)
|
|
261
|
+
if not project_path: # pragma: no cover
|
|
262
|
+
raise ValueError(f"Project '{actual_project_name}' not found")
|
|
263
|
+
|
|
264
|
+
return ProjectConfig(name=actual_project_name, home=Path(project_path))
|
|
225
265
|
|
|
226
266
|
|
|
227
267
|
# Create config manager
|
|
228
268
|
config_manager = ConfigManager()
|
|
229
269
|
|
|
230
|
-
#
|
|
231
|
-
|
|
270
|
+
# Export the app-level config
|
|
271
|
+
app_config: BasicMemoryConfig = config_manager.config
|
|
272
|
+
|
|
273
|
+
# Load project config for the default project (backward compatibility)
|
|
274
|
+
config: ProjectConfig = get_project_config()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def update_current_project(project_name: str) -> None:
|
|
278
|
+
"""Update the global config to use a different project.
|
|
279
|
+
|
|
280
|
+
This is used by the CLI when --project flag is specified.
|
|
281
|
+
"""
|
|
282
|
+
global config
|
|
283
|
+
config = get_project_config(project_name) # pragma: no cover
|
|
284
|
+
|
|
232
285
|
|
|
233
286
|
# setup logging to a single log file in user home directory
|
|
234
287
|
user_home = Path.home()
|
|
@@ -236,6 +289,7 @@ log_dir = user_home / DATA_DIR_NAME
|
|
|
236
289
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
237
290
|
|
|
238
291
|
|
|
292
|
+
# Process info for logging
|
|
239
293
|
def get_process_name(): # pragma: no cover
|
|
240
294
|
"""
|
|
241
295
|
get the type of process for logging
|
|
@@ -258,6 +312,9 @@ process_name = get_process_name()
|
|
|
258
312
|
_LOGGING_SETUP = False
|
|
259
313
|
|
|
260
314
|
|
|
315
|
+
# Logging
|
|
316
|
+
|
|
317
|
+
|
|
261
318
|
def setup_basic_memory_logging(): # pragma: no cover
|
|
262
319
|
"""Set up logging for basic-memory, ensuring it only happens once."""
|
|
263
320
|
global _LOGGING_SETUP
|
|
@@ -267,7 +324,7 @@ def setup_basic_memory_logging(): # pragma: no cover
|
|
|
267
324
|
return
|
|
268
325
|
|
|
269
326
|
setup_logging(
|
|
270
|
-
env=config.env,
|
|
327
|
+
env=config_manager.config.env,
|
|
271
328
|
home_dir=user_home, # Use user home for logs
|
|
272
329
|
log_level=config_manager.load_config().log_level,
|
|
273
330
|
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
|