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.

Files changed (107) hide show
  1. basic_memory/__init__.py +7 -1
  2. basic_memory/alembic/env.py +1 -1
  3. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  4. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -5
  5. basic_memory/api/app.py +43 -13
  6. basic_memory/api/routers/__init__.py +4 -2
  7. basic_memory/api/routers/directory_router.py +63 -0
  8. basic_memory/api/routers/importer_router.py +152 -0
  9. basic_memory/api/routers/knowledge_router.py +127 -38
  10. basic_memory/api/routers/management_router.py +78 -0
  11. basic_memory/api/routers/memory_router.py +4 -59
  12. basic_memory/api/routers/project_router.py +230 -0
  13. basic_memory/api/routers/prompt_router.py +260 -0
  14. basic_memory/api/routers/search_router.py +3 -21
  15. basic_memory/api/routers/utils.py +130 -0
  16. basic_memory/api/template_loader.py +292 -0
  17. basic_memory/cli/app.py +20 -21
  18. basic_memory/cli/commands/__init__.py +2 -1
  19. basic_memory/cli/commands/auth.py +136 -0
  20. basic_memory/cli/commands/db.py +3 -3
  21. basic_memory/cli/commands/import_chatgpt.py +31 -207
  22. basic_memory/cli/commands/import_claude_conversations.py +16 -142
  23. basic_memory/cli/commands/import_claude_projects.py +33 -143
  24. basic_memory/cli/commands/import_memory_json.py +26 -83
  25. basic_memory/cli/commands/mcp.py +71 -18
  26. basic_memory/cli/commands/project.py +99 -67
  27. basic_memory/cli/commands/status.py +19 -9
  28. basic_memory/cli/commands/sync.py +44 -58
  29. basic_memory/cli/main.py +1 -5
  30. basic_memory/config.py +145 -88
  31. basic_memory/db.py +6 -4
  32. basic_memory/deps.py +227 -30
  33. basic_memory/importers/__init__.py +27 -0
  34. basic_memory/importers/base.py +79 -0
  35. basic_memory/importers/chatgpt_importer.py +222 -0
  36. basic_memory/importers/claude_conversations_importer.py +172 -0
  37. basic_memory/importers/claude_projects_importer.py +148 -0
  38. basic_memory/importers/memory_json_importer.py +93 -0
  39. basic_memory/importers/utils.py +58 -0
  40. basic_memory/markdown/entity_parser.py +5 -2
  41. basic_memory/mcp/auth_provider.py +270 -0
  42. basic_memory/mcp/external_auth_provider.py +321 -0
  43. basic_memory/mcp/project_session.py +103 -0
  44. basic_memory/mcp/prompts/continue_conversation.py +18 -68
  45. basic_memory/mcp/prompts/recent_activity.py +19 -3
  46. basic_memory/mcp/prompts/search.py +14 -140
  47. basic_memory/mcp/prompts/utils.py +3 -3
  48. basic_memory/mcp/{tools → resources}/project_info.py +6 -2
  49. basic_memory/mcp/server.py +82 -8
  50. basic_memory/mcp/supabase_auth_provider.py +463 -0
  51. basic_memory/mcp/tools/__init__.py +20 -0
  52. basic_memory/mcp/tools/build_context.py +11 -1
  53. basic_memory/mcp/tools/canvas.py +15 -2
  54. basic_memory/mcp/tools/delete_note.py +12 -4
  55. basic_memory/mcp/tools/edit_note.py +297 -0
  56. basic_memory/mcp/tools/list_directory.py +154 -0
  57. basic_memory/mcp/tools/move_note.py +87 -0
  58. basic_memory/mcp/tools/project_management.py +300 -0
  59. basic_memory/mcp/tools/read_content.py +15 -6
  60. basic_memory/mcp/tools/read_note.py +17 -5
  61. basic_memory/mcp/tools/recent_activity.py +11 -2
  62. basic_memory/mcp/tools/search.py +10 -1
  63. basic_memory/mcp/tools/utils.py +137 -12
  64. basic_memory/mcp/tools/write_note.py +11 -15
  65. basic_memory/models/__init__.py +3 -2
  66. basic_memory/models/knowledge.py +16 -4
  67. basic_memory/models/project.py +80 -0
  68. basic_memory/models/search.py +8 -5
  69. basic_memory/repository/__init__.py +2 -0
  70. basic_memory/repository/entity_repository.py +8 -3
  71. basic_memory/repository/observation_repository.py +35 -3
  72. basic_memory/repository/project_info_repository.py +3 -2
  73. basic_memory/repository/project_repository.py +85 -0
  74. basic_memory/repository/relation_repository.py +8 -2
  75. basic_memory/repository/repository.py +107 -15
  76. basic_memory/repository/search_repository.py +87 -27
  77. basic_memory/schemas/__init__.py +6 -0
  78. basic_memory/schemas/directory.py +30 -0
  79. basic_memory/schemas/importer.py +34 -0
  80. basic_memory/schemas/memory.py +26 -12
  81. basic_memory/schemas/project_info.py +112 -2
  82. basic_memory/schemas/prompt.py +90 -0
  83. basic_memory/schemas/request.py +56 -2
  84. basic_memory/schemas/search.py +1 -1
  85. basic_memory/services/__init__.py +2 -1
  86. basic_memory/services/context_service.py +208 -95
  87. basic_memory/services/directory_service.py +167 -0
  88. basic_memory/services/entity_service.py +385 -5
  89. basic_memory/services/exceptions.py +6 -0
  90. basic_memory/services/file_service.py +14 -15
  91. basic_memory/services/initialization.py +144 -67
  92. basic_memory/services/link_resolver.py +16 -8
  93. basic_memory/services/project_service.py +548 -0
  94. basic_memory/services/search_service.py +77 -2
  95. basic_memory/sync/background_sync.py +25 -0
  96. basic_memory/sync/sync_service.py +10 -9
  97. basic_memory/sync/watch_service.py +63 -39
  98. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  99. basic_memory/templates/prompts/search.hbs +101 -0
  100. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/METADATA +23 -1
  101. basic_memory-0.13.0b1.dist-info/RECORD +132 -0
  102. basic_memory/api/routers/project_info_router.py +0 -274
  103. basic_memory/mcp/main.py +0 -24
  104. basic_memory-0.12.3.dist-info/RECORD +0 -100
  105. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/WHEEL +0 -0
  106. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/entry_points.txt +0 -0
  107. {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.sync.watch_service import WatchService
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=config.database_path, db_type=db.DatabaseType.FILESYSTEM
46
+ db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
45
47
  )
46
48
 
47
- entity_parser = EntityParser(config.home)
49
+ project_path = Path(project.path)
50
+ entity_parser = EntityParser(project_path)
48
51
  markdown_processor = MarkdownProcessor(entity_parser)
49
- file_service = FileService(config.home, markdown_processor)
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
- config=config,
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, watch: bool = False, console_status: 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
- # Start watching if requested
173
- if watch:
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
- # full sync - no progress bars in watch mode
182
- await sync_service.sync(config.home)
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
- # watch changes
185
- await watch_service.run() # pragma: no cover
198
+ # Display results
199
+ if verbose:
200
+ display_detailed_sync_results(knowledge_changes)
186
201
  else:
187
- # one time sync
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
- if not watch: # Don't show in watch mode as it would break the UI
230
- typer.echo(f"Syncing project: {config.project}")
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, watch=watch))
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
- class ProjectConfig(BaseSettings):
27
+ @dataclass
28
+ class ProjectConfig:
23
29
  """Configuration for a specific basic-memory project."""
24
30
 
25
- env: Environment = Field(default="dev", description="Environment name")
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 database_path(self) -> Path:
56
- """Get SQLite database path."""
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
- @field_validator("home")
64
- @classmethod
65
- def ensure_path_exists(cls, v: Path) -> Path: # pragma: no cover
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
- self.config_dir = Path.home() / DATA_DIR_NAME
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 get_project_path(self, project_name: Optional[str] = None) -> Path:
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
- """Get a project configuration for the specified project."""
206
- config_manager = ConfigManager()
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
- # Get project name from environment variable or use provided name or default
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
- update_permalinks_on_move = config_manager.load_config().update_permalinks_on_move
214
- try:
215
- project_path = config_manager.get_project_path(actual_project_name)
216
- return ProjectConfig(
217
- home=project_path,
218
- project=actual_project_name,
219
- update_permalinks_on_move=update_permalinks_on_move,
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
- except ValueError: # pragma: no cover
222
- logger.warning(f"Project '{actual_project_name}' not found, using default")
223
- project_path = config_manager.get_project_path(config_manager.default_project)
224
- return ProjectConfig(home=project_path, project=config_manager.default_project)
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
- # Load project config for current context
231
- config = get_project_config()
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: ProjectConfig, database_type=DatabaseType.FILESYSTEM
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
- await SearchRepository(session_maker).init_search_index()
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