basic-memory 0.16.1__py3-none-any.whl → 0.17.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of basic-memory might be problematic. Click here for more details.

Files changed (143) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/env.py +112 -26
  3. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  4. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +15 -3
  5. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +44 -36
  6. basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
  7. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  8. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +13 -0
  9. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  10. basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
  11. basic_memory/api/app.py +45 -24
  12. basic_memory/api/container.py +133 -0
  13. basic_memory/api/routers/knowledge_router.py +17 -5
  14. basic_memory/api/routers/project_router.py +68 -14
  15. basic_memory/api/routers/resource_router.py +37 -27
  16. basic_memory/api/routers/utils.py +53 -14
  17. basic_memory/api/v2/__init__.py +35 -0
  18. basic_memory/api/v2/routers/__init__.py +21 -0
  19. basic_memory/api/v2/routers/directory_router.py +93 -0
  20. basic_memory/api/v2/routers/importer_router.py +181 -0
  21. basic_memory/api/v2/routers/knowledge_router.py +427 -0
  22. basic_memory/api/v2/routers/memory_router.py +130 -0
  23. basic_memory/api/v2/routers/project_router.py +359 -0
  24. basic_memory/api/v2/routers/prompt_router.py +269 -0
  25. basic_memory/api/v2/routers/resource_router.py +286 -0
  26. basic_memory/api/v2/routers/search_router.py +73 -0
  27. basic_memory/cli/app.py +43 -7
  28. basic_memory/cli/auth.py +27 -4
  29. basic_memory/cli/commands/__init__.py +3 -1
  30. basic_memory/cli/commands/cloud/api_client.py +20 -5
  31. basic_memory/cli/commands/cloud/cloud_utils.py +13 -6
  32. basic_memory/cli/commands/cloud/rclone_commands.py +110 -14
  33. basic_memory/cli/commands/cloud/rclone_installer.py +18 -4
  34. basic_memory/cli/commands/cloud/upload.py +10 -3
  35. basic_memory/cli/commands/command_utils.py +52 -4
  36. basic_memory/cli/commands/db.py +78 -19
  37. basic_memory/cli/commands/format.py +198 -0
  38. basic_memory/cli/commands/import_chatgpt.py +12 -8
  39. basic_memory/cli/commands/import_claude_conversations.py +12 -8
  40. basic_memory/cli/commands/import_claude_projects.py +12 -8
  41. basic_memory/cli/commands/import_memory_json.py +12 -8
  42. basic_memory/cli/commands/mcp.py +8 -26
  43. basic_memory/cli/commands/project.py +22 -9
  44. basic_memory/cli/commands/status.py +3 -2
  45. basic_memory/cli/commands/telemetry.py +81 -0
  46. basic_memory/cli/container.py +84 -0
  47. basic_memory/cli/main.py +7 -0
  48. basic_memory/config.py +177 -77
  49. basic_memory/db.py +183 -77
  50. basic_memory/deps/__init__.py +293 -0
  51. basic_memory/deps/config.py +26 -0
  52. basic_memory/deps/db.py +56 -0
  53. basic_memory/deps/importers.py +200 -0
  54. basic_memory/deps/projects.py +238 -0
  55. basic_memory/deps/repositories.py +179 -0
  56. basic_memory/deps/services.py +480 -0
  57. basic_memory/deps.py +14 -409
  58. basic_memory/file_utils.py +212 -3
  59. basic_memory/ignore_utils.py +5 -5
  60. basic_memory/importers/base.py +40 -19
  61. basic_memory/importers/chatgpt_importer.py +17 -4
  62. basic_memory/importers/claude_conversations_importer.py +27 -12
  63. basic_memory/importers/claude_projects_importer.py +50 -14
  64. basic_memory/importers/memory_json_importer.py +36 -16
  65. basic_memory/importers/utils.py +5 -2
  66. basic_memory/markdown/entity_parser.py +62 -23
  67. basic_memory/markdown/markdown_processor.py +67 -4
  68. basic_memory/markdown/plugins.py +4 -2
  69. basic_memory/markdown/utils.py +10 -1
  70. basic_memory/mcp/async_client.py +1 -0
  71. basic_memory/mcp/clients/__init__.py +28 -0
  72. basic_memory/mcp/clients/directory.py +70 -0
  73. basic_memory/mcp/clients/knowledge.py +176 -0
  74. basic_memory/mcp/clients/memory.py +120 -0
  75. basic_memory/mcp/clients/project.py +89 -0
  76. basic_memory/mcp/clients/resource.py +71 -0
  77. basic_memory/mcp/clients/search.py +65 -0
  78. basic_memory/mcp/container.py +110 -0
  79. basic_memory/mcp/project_context.py +47 -33
  80. basic_memory/mcp/prompts/ai_assistant_guide.py +2 -2
  81. basic_memory/mcp/prompts/recent_activity.py +2 -2
  82. basic_memory/mcp/prompts/utils.py +3 -3
  83. basic_memory/mcp/server.py +58 -0
  84. basic_memory/mcp/tools/build_context.py +14 -14
  85. basic_memory/mcp/tools/canvas.py +34 -12
  86. basic_memory/mcp/tools/chatgpt_tools.py +4 -1
  87. basic_memory/mcp/tools/delete_note.py +31 -7
  88. basic_memory/mcp/tools/edit_note.py +14 -9
  89. basic_memory/mcp/tools/list_directory.py +7 -17
  90. basic_memory/mcp/tools/move_note.py +35 -31
  91. basic_memory/mcp/tools/project_management.py +29 -25
  92. basic_memory/mcp/tools/read_content.py +13 -3
  93. basic_memory/mcp/tools/read_note.py +24 -14
  94. basic_memory/mcp/tools/recent_activity.py +32 -38
  95. basic_memory/mcp/tools/search.py +17 -10
  96. basic_memory/mcp/tools/utils.py +28 -0
  97. basic_memory/mcp/tools/view_note.py +2 -1
  98. basic_memory/mcp/tools/write_note.py +37 -14
  99. basic_memory/models/knowledge.py +15 -2
  100. basic_memory/models/project.py +7 -1
  101. basic_memory/models/search.py +58 -2
  102. basic_memory/project_resolver.py +222 -0
  103. basic_memory/repository/entity_repository.py +210 -3
  104. basic_memory/repository/observation_repository.py +1 -0
  105. basic_memory/repository/postgres_search_repository.py +451 -0
  106. basic_memory/repository/project_repository.py +38 -1
  107. basic_memory/repository/relation_repository.py +58 -2
  108. basic_memory/repository/repository.py +1 -0
  109. basic_memory/repository/search_index_row.py +95 -0
  110. basic_memory/repository/search_repository.py +77 -615
  111. basic_memory/repository/search_repository_base.py +241 -0
  112. basic_memory/repository/sqlite_search_repository.py +437 -0
  113. basic_memory/runtime.py +61 -0
  114. basic_memory/schemas/base.py +36 -6
  115. basic_memory/schemas/directory.py +2 -1
  116. basic_memory/schemas/memory.py +9 -2
  117. basic_memory/schemas/project_info.py +2 -0
  118. basic_memory/schemas/response.py +84 -27
  119. basic_memory/schemas/search.py +5 -0
  120. basic_memory/schemas/sync_report.py +1 -1
  121. basic_memory/schemas/v2/__init__.py +27 -0
  122. basic_memory/schemas/v2/entity.py +133 -0
  123. basic_memory/schemas/v2/resource.py +47 -0
  124. basic_memory/services/context_service.py +219 -43
  125. basic_memory/services/directory_service.py +26 -11
  126. basic_memory/services/entity_service.py +68 -33
  127. basic_memory/services/file_service.py +131 -16
  128. basic_memory/services/initialization.py +51 -26
  129. basic_memory/services/link_resolver.py +1 -0
  130. basic_memory/services/project_service.py +68 -43
  131. basic_memory/services/search_service.py +75 -16
  132. basic_memory/sync/__init__.py +2 -1
  133. basic_memory/sync/coordinator.py +160 -0
  134. basic_memory/sync/sync_service.py +135 -115
  135. basic_memory/sync/watch_service.py +32 -12
  136. basic_memory/telemetry.py +249 -0
  137. basic_memory/utils.py +96 -75
  138. {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/METADATA +129 -5
  139. basic_memory-0.17.4.dist-info/RECORD +193 -0
  140. {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
  141. basic_memory-0.16.1.dist-info/RECORD +0 -148
  142. {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +0 -0
  143. {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
basic_memory/db.py CHANGED
@@ -1,11 +1,12 @@
1
1
  import asyncio
2
2
  import os
3
+ import sys
3
4
  from contextlib import asynccontextmanager
4
5
  from enum import Enum, auto
5
6
  from pathlib import Path
6
7
  from typing import AsyncGenerator, Optional
7
8
 
8
- from basic_memory.config import BasicMemoryConfig, ConfigManager
9
+ from basic_memory.config import BasicMemoryConfig, ConfigManager, DatabaseBackend
9
10
  from alembic import command
10
11
  from alembic.config import Config
11
12
 
@@ -20,12 +21,27 @@ from sqlalchemy.ext.asyncio import (
20
21
  )
21
22
  from sqlalchemy.pool import NullPool
22
23
 
23
- from basic_memory.repository.search_repository import SearchRepository
24
+ from basic_memory.repository.postgres_search_repository import PostgresSearchRepository
25
+ from basic_memory.repository.sqlite_search_repository import SQLiteSearchRepository
26
+
27
+ # -----------------------------------------------------------------------------
28
+ # Windows event loop policy
29
+ # -----------------------------------------------------------------------------
30
+ # On Windows, the default ProactorEventLoop has known rough edges with aiosqlite
31
+ # during shutdown/teardown (threads posting results to a loop that's closing),
32
+ # which can manifest as:
33
+ # - "RuntimeError: Event loop is closed"
34
+ # - "IndexError: pop from an empty deque"
35
+ #
36
+ # The SelectorEventLoop doesn't support subprocess operations, so code that uses
37
+ # asyncio.create_subprocess_shell() (like sync_service._quick_count_files) must
38
+ # detect Windows and use fallback implementations.
39
+ if sys.platform == "win32": # pragma: no cover
40
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
24
41
 
25
42
  # Module level state
26
43
  _engine: Optional[AsyncEngine] = None
27
44
  _session_maker: Optional[async_sessionmaker[AsyncSession]] = None
28
- _migrations_completed: bool = False
29
45
 
30
46
 
31
47
  class DatabaseType(Enum):
@@ -33,10 +49,41 @@ class DatabaseType(Enum):
33
49
 
34
50
  MEMORY = auto()
35
51
  FILESYSTEM = auto()
52
+ POSTGRES = auto()
36
53
 
37
54
  @classmethod
38
- def get_db_url(cls, db_path: Path, db_type: "DatabaseType") -> str:
39
- """Get SQLAlchemy URL for database path."""
55
+ def get_db_url(
56
+ cls, db_path: Path, db_type: "DatabaseType", config: Optional[BasicMemoryConfig] = None
57
+ ) -> str:
58
+ """Get SQLAlchemy URL for database path.
59
+
60
+ Args:
61
+ db_path: Path to SQLite database file (ignored for Postgres)
62
+ db_type: Type of database (MEMORY, FILESYSTEM, or POSTGRES)
63
+ config: Optional config to check for database backend and URL
64
+
65
+ Returns:
66
+ SQLAlchemy connection URL
67
+ """
68
+ # Load config if not provided
69
+ if config is None:
70
+ config = ConfigManager().config
71
+
72
+ # Handle explicit Postgres type
73
+ if db_type == cls.POSTGRES:
74
+ if not config.database_url:
75
+ raise ValueError("DATABASE_URL must be set when using Postgres backend")
76
+ logger.info(f"Using Postgres database: {config.database_url}")
77
+ return config.database_url
78
+
79
+ # Check if Postgres backend is configured (for backward compatibility)
80
+ if config.database_backend == DatabaseBackend.POSTGRES:
81
+ if not config.database_url:
82
+ raise ValueError("DATABASE_URL must be set when using Postgres backend")
83
+ logger.info(f"Using Postgres database: {config.database_url}")
84
+ return config.database_url
85
+
86
+ # SQLite databases
40
87
  if db_type == cls.MEMORY:
41
88
  logger.info("Using in-memory SQLite database")
42
89
  return "sqlite+aiosqlite://"
@@ -64,7 +111,14 @@ async def scoped_session(
64
111
  factory = get_scoped_session_factory(session_maker)
65
112
  session = factory()
66
113
  try:
67
- await session.execute(text("PRAGMA foreign_keys=ON"))
114
+ # Only enable foreign keys for SQLite (Postgres has them enabled by default)
115
+ # Detect database type from session's bind (engine) dialect
116
+ engine = session.get_bind()
117
+ dialect_name = engine.dialect.name
118
+
119
+ if dialect_name == "sqlite":
120
+ await session.execute(text("PRAGMA foreign_keys=ON"))
121
+
68
122
  yield session
69
123
  await session.commit()
70
124
  except Exception:
@@ -103,13 +157,16 @@ def _configure_sqlite_connection(dbapi_conn, enable_wal: bool = True) -> None:
103
157
  cursor.close()
104
158
 
105
159
 
106
- def _create_engine_and_session(
107
- db_path: Path, db_type: DatabaseType = DatabaseType.FILESYSTEM
108
- ) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]:
109
- """Internal helper to create engine and session maker."""
110
- db_url = DatabaseType.get_db_url(db_path, db_type)
111
- logger.debug(f"Creating engine for db_url: {db_url}")
160
+ def _create_sqlite_engine(db_url: str, db_type: DatabaseType) -> AsyncEngine:
161
+ """Create SQLite async engine with appropriate configuration.
112
162
 
163
+ Args:
164
+ db_url: SQLite connection URL
165
+ db_type: Database type (MEMORY or FILESYSTEM)
166
+
167
+ Returns:
168
+ Configured async engine for SQLite
169
+ """
113
170
  # Configure connection args with Windows-specific settings
114
171
  connect_args: dict[str, bool | float | None] = {"check_same_thread": False}
115
172
 
@@ -146,6 +203,73 @@ def _create_engine_and_session(
146
203
  """Enable WAL mode on each connection."""
147
204
  _configure_sqlite_connection(dbapi_conn, enable_wal=enable_wal)
148
205
 
206
+ return engine
207
+
208
+
209
+ def _create_postgres_engine(db_url: str, config: BasicMemoryConfig) -> AsyncEngine:
210
+ """Create Postgres async engine with appropriate configuration.
211
+
212
+ Args:
213
+ db_url: Postgres connection URL (postgresql+asyncpg://...)
214
+ config: BasicMemoryConfig with pool settings
215
+
216
+ Returns:
217
+ Configured async engine for Postgres
218
+ """
219
+ # Use NullPool connection issues.
220
+ # Assume connection pooler like PgBouncer handles connection pooling.
221
+ engine = create_async_engine(
222
+ db_url,
223
+ echo=False,
224
+ poolclass=NullPool, # No pooling - fresh connection per request
225
+ connect_args={
226
+ # Disable statement cache to avoid issues with prepared statements on reconnect
227
+ "statement_cache_size": 0,
228
+ # Allow 30s for commands (Neon cold start can take 2-5s, sometimes longer)
229
+ "command_timeout": 30,
230
+ # Allow 30s for initial connection (Neon wake-up time)
231
+ "timeout": 30,
232
+ "server_settings": {
233
+ "application_name": "basic-memory",
234
+ # Statement timeout for queries (30s to allow for cold start)
235
+ "statement_timeout": "30s",
236
+ },
237
+ },
238
+ )
239
+ logger.debug("Created Postgres engine with NullPool (no connection pooling)")
240
+
241
+ return engine
242
+
243
+
244
+ def _create_engine_and_session(
245
+ db_path: Path,
246
+ db_type: DatabaseType = DatabaseType.FILESYSTEM,
247
+ config: Optional[BasicMemoryConfig] = None,
248
+ ) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]:
249
+ """Internal helper to create engine and session maker.
250
+
251
+ Args:
252
+ db_path: Path to database file (used for SQLite, ignored for Postgres)
253
+ db_type: Type of database (MEMORY, FILESYSTEM, or POSTGRES)
254
+ config: Optional explicit config. If not provided, reads from ConfigManager.
255
+ Prefer passing explicitly from composition roots.
256
+
257
+ Returns:
258
+ Tuple of (engine, session_maker)
259
+ """
260
+ # Prefer explicit parameter; fall back to ConfigManager for backwards compatibility
261
+ if config is None:
262
+ config = ConfigManager().config
263
+ db_url = DatabaseType.get_db_url(db_path, db_type, config)
264
+ logger.debug(f"Creating engine for db_url: {db_url}")
265
+
266
+ # Delegate to backend-specific engine creation
267
+ # Check explicit POSTGRES type first, then config setting
268
+ if db_type == DatabaseType.POSTGRES or config.database_backend == DatabaseBackend.POSTGRES:
269
+ engine = _create_postgres_engine(db_url, config)
270
+ else:
271
+ engine = _create_sqlite_engine(db_url, db_type)
272
+
149
273
  session_maker = async_sessionmaker(engine, expire_on_commit=False)
150
274
  return engine, session_maker
151
275
 
@@ -154,17 +278,29 @@ async def get_or_create_db(
154
278
  db_path: Path,
155
279
  db_type: DatabaseType = DatabaseType.FILESYSTEM,
156
280
  ensure_migrations: bool = True,
281
+ config: Optional[BasicMemoryConfig] = None,
157
282
  ) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: # pragma: no cover
158
- """Get or create database engine and session maker."""
283
+ """Get or create database engine and session maker.
284
+
285
+ Args:
286
+ db_path: Path to database file
287
+ db_type: Type of database
288
+ ensure_migrations: Whether to run migrations
289
+ config: Optional explicit config. If not provided, reads from ConfigManager.
290
+ Prefer passing explicitly from composition roots.
291
+ """
159
292
  global _engine, _session_maker
160
293
 
294
+ # Prefer explicit parameter; fall back to ConfigManager for backwards compatibility
295
+ if config is None:
296
+ config = ConfigManager().config
297
+
161
298
  if _engine is None:
162
- _engine, _session_maker = _create_engine_and_session(db_path, db_type)
299
+ _engine, _session_maker = _create_engine_and_session(db_path, db_type, config)
163
300
 
164
301
  # Run migrations automatically unless explicitly disabled
165
302
  if ensure_migrations:
166
- app_config = ConfigManager().config
167
- await run_migrations(app_config, db_type)
303
+ await run_migrations(config, db_type)
168
304
 
169
305
  # These checks should never fail since we just created the engine and session maker
170
306
  # if they were None, but we'll check anyway for the type checker
@@ -181,70 +317,37 @@ async def get_or_create_db(
181
317
 
182
318
  async def shutdown_db() -> None: # pragma: no cover
183
319
  """Clean up database connections."""
184
- global _engine, _session_maker, _migrations_completed
320
+ global _engine, _session_maker
185
321
 
186
322
  if _engine:
187
323
  await _engine.dispose()
188
324
  _engine = None
189
325
  _session_maker = None
190
- _migrations_completed = False
191
326
 
192
327
 
193
328
  @asynccontextmanager
194
329
  async def engine_session_factory(
195
330
  db_path: Path,
196
331
  db_type: DatabaseType = DatabaseType.MEMORY,
332
+ config: Optional[BasicMemoryConfig] = None,
197
333
  ) -> AsyncGenerator[tuple[AsyncEngine, async_sessionmaker[AsyncSession]], None]:
198
334
  """Create engine and session factory.
199
335
 
200
336
  Note: This is primarily used for testing where we want a fresh database
201
337
  for each test. For production use, use get_or_create_db() instead.
202
- """
203
-
204
- global _engine, _session_maker, _migrations_completed
205
338
 
206
- db_url = DatabaseType.get_db_url(db_path, db_type)
207
- logger.debug(f"Creating engine for db_url: {db_url}")
208
-
209
- # Configure connection args with Windows-specific settings
210
- connect_args: dict[str, bool | float | None] = {"check_same_thread": False}
211
-
212
- # Add Windows-specific parameters to improve reliability
213
- if os.name == "nt": # Windows
214
- connect_args.update(
215
- {
216
- "timeout": 30.0, # Increase timeout to 30 seconds for Windows
217
- "isolation_level": None, # Use autocommit mode
218
- }
219
- )
220
- # Use NullPool for Windows filesystem databases to avoid connection pooling issues
221
- # Important: Do NOT use NullPool for in-memory databases as it will destroy the database
222
- # between connections
223
- if db_type == DatabaseType.FILESYSTEM:
224
- _engine = create_async_engine(
225
- db_url,
226
- connect_args=connect_args,
227
- poolclass=NullPool, # Disable connection pooling on Windows
228
- echo=False,
229
- )
230
- else:
231
- # In-memory databases need connection pooling to maintain state
232
- _engine = create_async_engine(db_url, connect_args=connect_args)
233
- else:
234
- _engine = create_async_engine(db_url, connect_args=connect_args)
339
+ Args:
340
+ db_path: Path to database file
341
+ db_type: Type of database
342
+ config: Optional explicit config. If not provided, reads from ConfigManager.
343
+ """
235
344
 
236
- # Enable WAL mode for better concurrency and reliability
237
- # Note: WAL mode is not supported for in-memory databases
238
- enable_wal = db_type != DatabaseType.MEMORY
345
+ global _engine, _session_maker
239
346
 
240
- @event.listens_for(_engine.sync_engine, "connect")
241
- def enable_wal_mode(dbapi_conn, connection_record):
242
- """Enable WAL mode on each connection."""
243
- _configure_sqlite_connection(dbapi_conn, enable_wal=enable_wal)
347
+ # Use the same helper function as production code
348
+ _engine, _session_maker = _create_engine_and_session(db_path, db_type, config)
244
349
 
245
350
  try:
246
- _session_maker = async_sessionmaker(_engine, expire_on_commit=False)
247
-
248
351
  # Verify that engine and session maker are initialized
249
352
  if _engine is None: # pragma: no cover
250
353
  logger.error("Database engine is None in engine_session_factory")
@@ -260,20 +363,16 @@ async def engine_session_factory(
260
363
  await _engine.dispose()
261
364
  _engine = None
262
365
  _session_maker = None
263
- _migrations_completed = False
264
366
 
265
367
 
266
368
  async def run_migrations(
267
- app_config: BasicMemoryConfig, database_type=DatabaseType.FILESYSTEM, force: bool = False
369
+ app_config: BasicMemoryConfig, database_type=DatabaseType.FILESYSTEM
268
370
  ): # pragma: no cover
269
- """Run any pending alembic migrations."""
270
- global _migrations_completed
271
-
272
- # Skip if migrations already completed unless forced
273
- if _migrations_completed and not force:
274
- logger.debug("Migrations already completed in this session, skipping")
275
- return
371
+ """Run any pending alembic migrations.
276
372
 
373
+ Note: Alembic tracks which migrations have been applied via the alembic_version table,
374
+ so it's safe to call this multiple times - it will only run pending migrations.
375
+ """
277
376
  logger.info("Running database migrations...")
278
377
  try:
279
378
  # Get the absolute path to the alembic directory relative to this file
@@ -288,9 +387,11 @@ async def run_migrations(
288
387
  )
289
388
  config.set_main_option("timezone", "UTC")
290
389
  config.set_main_option("revision_environment", "false")
291
- config.set_main_option(
292
- "sqlalchemy.url", DatabaseType.get_db_url(app_config.database_path, database_type)
293
- )
390
+
391
+ # Get the correct database URL based on backend configuration
392
+ # No URL conversion needed - env.py now handles both async and sync engines
393
+ db_url = DatabaseType.get_db_url(app_config.database_path, database_type, app_config)
394
+ config.set_main_option("sqlalchemy.url", db_url)
294
395
 
295
396
  command.upgrade(config, "head")
296
397
  logger.info("Migrations completed successfully")
@@ -301,12 +402,17 @@ async def run_migrations(
301
402
  else:
302
403
  session_maker = _session_maker
303
404
 
304
- # initialize the search Index schema
305
- # the project_id is not used for init_search_index, so we pass a dummy value
306
- await SearchRepository(session_maker, 1).init_search_index()
307
-
308
- # Mark migrations as completed
309
- _migrations_completed = True
405
+ # Initialize the search index schema
406
+ # For SQLite: Create FTS5 virtual table
407
+ # For Postgres: No-op (tsvector column added by migrations)
408
+ # The project_id is not used for init_search_index, so we pass a dummy value
409
+ if (
410
+ database_type == DatabaseType.POSTGRES
411
+ or app_config.database_backend == DatabaseBackend.POSTGRES
412
+ ):
413
+ await PostgresSearchRepository(session_maker, 1).init_search_index()
414
+ else:
415
+ await SQLiteSearchRepository(session_maker, 1).init_search_index()
310
416
  except Exception as e: # pragma: no cover
311
417
  logger.error(f"Error running migrations: {e}")
312
418
  raise
@@ -0,0 +1,293 @@
1
+ """Dependency injection for basic-memory.
2
+
3
+ This package provides FastAPI dependencies organized by feature:
4
+ - config: Application configuration
5
+ - db: Database/session management
6
+ - projects: Project resolution and config
7
+ - repositories: Data access layer
8
+ - services: Business logic layer
9
+ - importers: Import functionality
10
+
11
+ For backwards compatibility, all dependencies are re-exported from this module.
12
+ New code should import from specific submodules to reduce coupling.
13
+ """
14
+
15
+ # Re-export everything for backwards compatibility
16
+ # Eventually, callers should import from specific submodules
17
+
18
+ from basic_memory.deps.config import (
19
+ get_app_config,
20
+ AppConfigDep,
21
+ )
22
+
23
+ from basic_memory.deps.db import (
24
+ get_engine_factory,
25
+ EngineFactoryDep,
26
+ get_session_maker,
27
+ SessionMakerDep,
28
+ )
29
+
30
+ from basic_memory.deps.projects import (
31
+ get_project_repository,
32
+ ProjectRepositoryDep,
33
+ ProjectPathDep,
34
+ get_project_id,
35
+ ProjectIdDep,
36
+ get_project_config,
37
+ ProjectConfigDep,
38
+ validate_project_id,
39
+ ProjectIdPathDep,
40
+ get_project_config_v2,
41
+ ProjectConfigV2Dep,
42
+ validate_project_external_id,
43
+ ProjectExternalIdPathDep,
44
+ get_project_config_v2_external,
45
+ ProjectConfigV2ExternalDep,
46
+ )
47
+
48
+ from basic_memory.deps.repositories import (
49
+ get_entity_repository,
50
+ EntityRepositoryDep,
51
+ get_entity_repository_v2,
52
+ EntityRepositoryV2Dep,
53
+ get_entity_repository_v2_external,
54
+ EntityRepositoryV2ExternalDep,
55
+ get_observation_repository,
56
+ ObservationRepositoryDep,
57
+ get_observation_repository_v2,
58
+ ObservationRepositoryV2Dep,
59
+ get_observation_repository_v2_external,
60
+ ObservationRepositoryV2ExternalDep,
61
+ get_relation_repository,
62
+ RelationRepositoryDep,
63
+ get_relation_repository_v2,
64
+ RelationRepositoryV2Dep,
65
+ get_relation_repository_v2_external,
66
+ RelationRepositoryV2ExternalDep,
67
+ get_search_repository,
68
+ SearchRepositoryDep,
69
+ get_search_repository_v2,
70
+ SearchRepositoryV2Dep,
71
+ get_search_repository_v2_external,
72
+ SearchRepositoryV2ExternalDep,
73
+ )
74
+
75
+ from basic_memory.deps.services import (
76
+ get_entity_parser,
77
+ EntityParserDep,
78
+ get_entity_parser_v2,
79
+ EntityParserV2Dep,
80
+ get_entity_parser_v2_external,
81
+ EntityParserV2ExternalDep,
82
+ get_markdown_processor,
83
+ MarkdownProcessorDep,
84
+ get_markdown_processor_v2,
85
+ MarkdownProcessorV2Dep,
86
+ get_markdown_processor_v2_external,
87
+ MarkdownProcessorV2ExternalDep,
88
+ get_file_service,
89
+ FileServiceDep,
90
+ get_file_service_v2,
91
+ FileServiceV2Dep,
92
+ get_file_service_v2_external,
93
+ FileServiceV2ExternalDep,
94
+ get_search_service,
95
+ SearchServiceDep,
96
+ get_search_service_v2,
97
+ SearchServiceV2Dep,
98
+ get_search_service_v2_external,
99
+ SearchServiceV2ExternalDep,
100
+ get_link_resolver,
101
+ LinkResolverDep,
102
+ get_link_resolver_v2,
103
+ LinkResolverV2Dep,
104
+ get_link_resolver_v2_external,
105
+ LinkResolverV2ExternalDep,
106
+ get_entity_service,
107
+ EntityServiceDep,
108
+ get_entity_service_v2,
109
+ EntityServiceV2Dep,
110
+ get_entity_service_v2_external,
111
+ EntityServiceV2ExternalDep,
112
+ get_context_service,
113
+ ContextServiceDep,
114
+ get_context_service_v2,
115
+ ContextServiceV2Dep,
116
+ get_context_service_v2_external,
117
+ ContextServiceV2ExternalDep,
118
+ get_sync_service,
119
+ SyncServiceDep,
120
+ get_sync_service_v2,
121
+ SyncServiceV2Dep,
122
+ get_sync_service_v2_external,
123
+ SyncServiceV2ExternalDep,
124
+ get_project_service,
125
+ ProjectServiceDep,
126
+ get_directory_service,
127
+ DirectoryServiceDep,
128
+ get_directory_service_v2,
129
+ DirectoryServiceV2Dep,
130
+ get_directory_service_v2_external,
131
+ DirectoryServiceV2ExternalDep,
132
+ )
133
+
134
+ from basic_memory.deps.importers import (
135
+ get_chatgpt_importer,
136
+ ChatGPTImporterDep,
137
+ get_chatgpt_importer_v2,
138
+ ChatGPTImporterV2Dep,
139
+ get_chatgpt_importer_v2_external,
140
+ ChatGPTImporterV2ExternalDep,
141
+ get_claude_conversations_importer,
142
+ ClaudeConversationsImporterDep,
143
+ get_claude_conversations_importer_v2,
144
+ ClaudeConversationsImporterV2Dep,
145
+ get_claude_conversations_importer_v2_external,
146
+ ClaudeConversationsImporterV2ExternalDep,
147
+ get_claude_projects_importer,
148
+ ClaudeProjectsImporterDep,
149
+ get_claude_projects_importer_v2,
150
+ ClaudeProjectsImporterV2Dep,
151
+ get_claude_projects_importer_v2_external,
152
+ ClaudeProjectsImporterV2ExternalDep,
153
+ get_memory_json_importer,
154
+ MemoryJsonImporterDep,
155
+ get_memory_json_importer_v2,
156
+ MemoryJsonImporterV2Dep,
157
+ get_memory_json_importer_v2_external,
158
+ MemoryJsonImporterV2ExternalDep,
159
+ )
160
+
161
+ __all__ = [
162
+ # Config
163
+ "get_app_config",
164
+ "AppConfigDep",
165
+ # Database
166
+ "get_engine_factory",
167
+ "EngineFactoryDep",
168
+ "get_session_maker",
169
+ "SessionMakerDep",
170
+ # Projects
171
+ "get_project_repository",
172
+ "ProjectRepositoryDep",
173
+ "ProjectPathDep",
174
+ "get_project_id",
175
+ "ProjectIdDep",
176
+ "get_project_config",
177
+ "ProjectConfigDep",
178
+ "validate_project_id",
179
+ "ProjectIdPathDep",
180
+ "get_project_config_v2",
181
+ "ProjectConfigV2Dep",
182
+ "validate_project_external_id",
183
+ "ProjectExternalIdPathDep",
184
+ "get_project_config_v2_external",
185
+ "ProjectConfigV2ExternalDep",
186
+ # Repositories
187
+ "get_entity_repository",
188
+ "EntityRepositoryDep",
189
+ "get_entity_repository_v2",
190
+ "EntityRepositoryV2Dep",
191
+ "get_entity_repository_v2_external",
192
+ "EntityRepositoryV2ExternalDep",
193
+ "get_observation_repository",
194
+ "ObservationRepositoryDep",
195
+ "get_observation_repository_v2",
196
+ "ObservationRepositoryV2Dep",
197
+ "get_observation_repository_v2_external",
198
+ "ObservationRepositoryV2ExternalDep",
199
+ "get_relation_repository",
200
+ "RelationRepositoryDep",
201
+ "get_relation_repository_v2",
202
+ "RelationRepositoryV2Dep",
203
+ "get_relation_repository_v2_external",
204
+ "RelationRepositoryV2ExternalDep",
205
+ "get_search_repository",
206
+ "SearchRepositoryDep",
207
+ "get_search_repository_v2",
208
+ "SearchRepositoryV2Dep",
209
+ "get_search_repository_v2_external",
210
+ "SearchRepositoryV2ExternalDep",
211
+ # Services
212
+ "get_entity_parser",
213
+ "EntityParserDep",
214
+ "get_entity_parser_v2",
215
+ "EntityParserV2Dep",
216
+ "get_entity_parser_v2_external",
217
+ "EntityParserV2ExternalDep",
218
+ "get_markdown_processor",
219
+ "MarkdownProcessorDep",
220
+ "get_markdown_processor_v2",
221
+ "MarkdownProcessorV2Dep",
222
+ "get_markdown_processor_v2_external",
223
+ "MarkdownProcessorV2ExternalDep",
224
+ "get_file_service",
225
+ "FileServiceDep",
226
+ "get_file_service_v2",
227
+ "FileServiceV2Dep",
228
+ "get_file_service_v2_external",
229
+ "FileServiceV2ExternalDep",
230
+ "get_search_service",
231
+ "SearchServiceDep",
232
+ "get_search_service_v2",
233
+ "SearchServiceV2Dep",
234
+ "get_search_service_v2_external",
235
+ "SearchServiceV2ExternalDep",
236
+ "get_link_resolver",
237
+ "LinkResolverDep",
238
+ "get_link_resolver_v2",
239
+ "LinkResolverV2Dep",
240
+ "get_link_resolver_v2_external",
241
+ "LinkResolverV2ExternalDep",
242
+ "get_entity_service",
243
+ "EntityServiceDep",
244
+ "get_entity_service_v2",
245
+ "EntityServiceV2Dep",
246
+ "get_entity_service_v2_external",
247
+ "EntityServiceV2ExternalDep",
248
+ "get_context_service",
249
+ "ContextServiceDep",
250
+ "get_context_service_v2",
251
+ "ContextServiceV2Dep",
252
+ "get_context_service_v2_external",
253
+ "ContextServiceV2ExternalDep",
254
+ "get_sync_service",
255
+ "SyncServiceDep",
256
+ "get_sync_service_v2",
257
+ "SyncServiceV2Dep",
258
+ "get_sync_service_v2_external",
259
+ "SyncServiceV2ExternalDep",
260
+ "get_project_service",
261
+ "ProjectServiceDep",
262
+ "get_directory_service",
263
+ "DirectoryServiceDep",
264
+ "get_directory_service_v2",
265
+ "DirectoryServiceV2Dep",
266
+ "get_directory_service_v2_external",
267
+ "DirectoryServiceV2ExternalDep",
268
+ # Importers
269
+ "get_chatgpt_importer",
270
+ "ChatGPTImporterDep",
271
+ "get_chatgpt_importer_v2",
272
+ "ChatGPTImporterV2Dep",
273
+ "get_chatgpt_importer_v2_external",
274
+ "ChatGPTImporterV2ExternalDep",
275
+ "get_claude_conversations_importer",
276
+ "ClaudeConversationsImporterDep",
277
+ "get_claude_conversations_importer_v2",
278
+ "ClaudeConversationsImporterV2Dep",
279
+ "get_claude_conversations_importer_v2_external",
280
+ "ClaudeConversationsImporterV2ExternalDep",
281
+ "get_claude_projects_importer",
282
+ "ClaudeProjectsImporterDep",
283
+ "get_claude_projects_importer_v2",
284
+ "ClaudeProjectsImporterV2Dep",
285
+ "get_claude_projects_importer_v2_external",
286
+ "ClaudeProjectsImporterV2ExternalDep",
287
+ "get_memory_json_importer",
288
+ "MemoryJsonImporterDep",
289
+ "get_memory_json_importer_v2",
290
+ "MemoryJsonImporterV2Dep",
291
+ "get_memory_json_importer_v2_external",
292
+ "MemoryJsonImporterV2ExternalDep",
293
+ ]
@@ -0,0 +1,26 @@
1
+ """Configuration dependency injection for basic-memory.
2
+
3
+ This module provides configuration-related dependencies.
4
+ Note: Long-term goal is to minimize direct ConfigManager access
5
+ and inject config from composition roots instead.
6
+ """
7
+
8
+ from typing import Annotated
9
+
10
+ from fastapi import Depends
11
+
12
+ from basic_memory.config import BasicMemoryConfig, ConfigManager
13
+
14
+
15
+ def get_app_config() -> BasicMemoryConfig: # pragma: no cover
16
+ """Get the application configuration.
17
+
18
+ Note: This is a transitional dependency. The goal is for composition roots
19
+ to read ConfigManager and inject config explicitly. During migration,
20
+ this provides the same behavior as before.
21
+ """
22
+ app_config = ConfigManager().config
23
+ return app_config
24
+
25
+
26
+ AppConfigDep = Annotated[BasicMemoryConfig, Depends(get_app_config)]