basic-memory 0.2.12__py3-none-any.whl → 0.16.1__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 (149) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +27 -3
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  6. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  7. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -0
  8. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  9. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  10. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  11. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +100 -0
  12. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  13. basic_memory/api/app.py +63 -31
  14. basic_memory/api/routers/__init__.py +4 -1
  15. basic_memory/api/routers/directory_router.py +84 -0
  16. basic_memory/api/routers/importer_router.py +152 -0
  17. basic_memory/api/routers/knowledge_router.py +165 -28
  18. basic_memory/api/routers/management_router.py +80 -0
  19. basic_memory/api/routers/memory_router.py +28 -67
  20. basic_memory/api/routers/project_router.py +406 -0
  21. basic_memory/api/routers/prompt_router.py +260 -0
  22. basic_memory/api/routers/resource_router.py +219 -14
  23. basic_memory/api/routers/search_router.py +21 -13
  24. basic_memory/api/routers/utils.py +130 -0
  25. basic_memory/api/template_loader.py +292 -0
  26. basic_memory/cli/app.py +52 -1
  27. basic_memory/cli/auth.py +277 -0
  28. basic_memory/cli/commands/__init__.py +13 -2
  29. basic_memory/cli/commands/cloud/__init__.py +6 -0
  30. basic_memory/cli/commands/cloud/api_client.py +112 -0
  31. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  32. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  33. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  34. basic_memory/cli/commands/cloud/rclone_commands.py +301 -0
  35. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  36. basic_memory/cli/commands/cloud/rclone_installer.py +249 -0
  37. basic_memory/cli/commands/cloud/upload.py +233 -0
  38. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  39. basic_memory/cli/commands/command_utils.py +51 -0
  40. basic_memory/cli/commands/db.py +26 -7
  41. basic_memory/cli/commands/import_chatgpt.py +83 -0
  42. basic_memory/cli/commands/import_claude_conversations.py +86 -0
  43. basic_memory/cli/commands/import_claude_projects.py +85 -0
  44. basic_memory/cli/commands/import_memory_json.py +35 -92
  45. basic_memory/cli/commands/mcp.py +84 -10
  46. basic_memory/cli/commands/project.py +876 -0
  47. basic_memory/cli/commands/status.py +47 -30
  48. basic_memory/cli/commands/tool.py +341 -0
  49. basic_memory/cli/main.py +13 -6
  50. basic_memory/config.py +481 -22
  51. basic_memory/db.py +192 -32
  52. basic_memory/deps.py +252 -22
  53. basic_memory/file_utils.py +113 -58
  54. basic_memory/ignore_utils.py +297 -0
  55. basic_memory/importers/__init__.py +27 -0
  56. basic_memory/importers/base.py +79 -0
  57. basic_memory/importers/chatgpt_importer.py +232 -0
  58. basic_memory/importers/claude_conversations_importer.py +177 -0
  59. basic_memory/importers/claude_projects_importer.py +148 -0
  60. basic_memory/importers/memory_json_importer.py +108 -0
  61. basic_memory/importers/utils.py +58 -0
  62. basic_memory/markdown/entity_parser.py +143 -23
  63. basic_memory/markdown/markdown_processor.py +3 -3
  64. basic_memory/markdown/plugins.py +39 -21
  65. basic_memory/markdown/schemas.py +1 -1
  66. basic_memory/markdown/utils.py +28 -13
  67. basic_memory/mcp/async_client.py +134 -4
  68. basic_memory/mcp/project_context.py +141 -0
  69. basic_memory/mcp/prompts/__init__.py +19 -0
  70. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  71. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  72. basic_memory/mcp/prompts/recent_activity.py +188 -0
  73. basic_memory/mcp/prompts/search.py +57 -0
  74. basic_memory/mcp/prompts/utils.py +162 -0
  75. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  76. basic_memory/mcp/resources/project_info.py +71 -0
  77. basic_memory/mcp/server.py +7 -13
  78. basic_memory/mcp/tools/__init__.py +33 -21
  79. basic_memory/mcp/tools/build_context.py +120 -0
  80. basic_memory/mcp/tools/canvas.py +130 -0
  81. basic_memory/mcp/tools/chatgpt_tools.py +187 -0
  82. basic_memory/mcp/tools/delete_note.py +225 -0
  83. basic_memory/mcp/tools/edit_note.py +320 -0
  84. basic_memory/mcp/tools/list_directory.py +167 -0
  85. basic_memory/mcp/tools/move_note.py +545 -0
  86. basic_memory/mcp/tools/project_management.py +200 -0
  87. basic_memory/mcp/tools/read_content.py +271 -0
  88. basic_memory/mcp/tools/read_note.py +255 -0
  89. basic_memory/mcp/tools/recent_activity.py +534 -0
  90. basic_memory/mcp/tools/search.py +369 -14
  91. basic_memory/mcp/tools/utils.py +374 -16
  92. basic_memory/mcp/tools/view_note.py +77 -0
  93. basic_memory/mcp/tools/write_note.py +207 -0
  94. basic_memory/models/__init__.py +3 -2
  95. basic_memory/models/knowledge.py +67 -15
  96. basic_memory/models/project.py +87 -0
  97. basic_memory/models/search.py +10 -6
  98. basic_memory/repository/__init__.py +2 -0
  99. basic_memory/repository/entity_repository.py +229 -7
  100. basic_memory/repository/observation_repository.py +35 -3
  101. basic_memory/repository/project_info_repository.py +10 -0
  102. basic_memory/repository/project_repository.py +103 -0
  103. basic_memory/repository/relation_repository.py +21 -2
  104. basic_memory/repository/repository.py +147 -29
  105. basic_memory/repository/search_repository.py +437 -59
  106. basic_memory/schemas/__init__.py +22 -9
  107. basic_memory/schemas/base.py +97 -8
  108. basic_memory/schemas/cloud.py +50 -0
  109. basic_memory/schemas/directory.py +30 -0
  110. basic_memory/schemas/importer.py +35 -0
  111. basic_memory/schemas/memory.py +188 -23
  112. basic_memory/schemas/project_info.py +211 -0
  113. basic_memory/schemas/prompt.py +90 -0
  114. basic_memory/schemas/request.py +57 -3
  115. basic_memory/schemas/response.py +9 -1
  116. basic_memory/schemas/search.py +33 -35
  117. basic_memory/schemas/sync_report.py +72 -0
  118. basic_memory/services/__init__.py +2 -1
  119. basic_memory/services/context_service.py +251 -106
  120. basic_memory/services/directory_service.py +295 -0
  121. basic_memory/services/entity_service.py +595 -60
  122. basic_memory/services/exceptions.py +21 -0
  123. basic_memory/services/file_service.py +284 -30
  124. basic_memory/services/initialization.py +191 -0
  125. basic_memory/services/link_resolver.py +50 -56
  126. basic_memory/services/project_service.py +863 -0
  127. basic_memory/services/search_service.py +172 -34
  128. basic_memory/sync/__init__.py +3 -2
  129. basic_memory/sync/background_sync.py +26 -0
  130. basic_memory/sync/sync_service.py +1176 -96
  131. basic_memory/sync/watch_service.py +412 -135
  132. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  133. basic_memory/templates/prompts/search.hbs +101 -0
  134. basic_memory/utils.py +388 -28
  135. basic_memory-0.16.1.dist-info/METADATA +493 -0
  136. basic_memory-0.16.1.dist-info/RECORD +148 -0
  137. {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/entry_points.txt +1 -0
  138. basic_memory/alembic/README +0 -1
  139. basic_memory/cli/commands/sync.py +0 -203
  140. basic_memory/mcp/tools/knowledge.py +0 -56
  141. basic_memory/mcp/tools/memory.py +0 -151
  142. basic_memory/mcp/tools/notes.py +0 -122
  143. basic_memory/schemas/discovery.py +0 -28
  144. basic_memory/sync/file_change_scanner.py +0 -158
  145. basic_memory/sync/utils.py +0 -34
  146. basic_memory-0.2.12.dist-info/METADATA +0 -291
  147. basic_memory-0.2.12.dist-info/RECORD +0 -78
  148. {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/WHEEL +0 -0
  149. {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/licenses/LICENSE +0 -0
basic_memory/db.py CHANGED
@@ -1,11 +1,16 @@
1
1
  import asyncio
2
+ import os
2
3
  from contextlib import asynccontextmanager
3
4
  from enum import Enum, auto
4
5
  from pathlib import Path
5
6
  from typing import AsyncGenerator, Optional
6
7
 
8
+ from basic_memory.config import BasicMemoryConfig, ConfigManager
9
+ from alembic import command
10
+ from alembic.config import Config
11
+
7
12
  from loguru import logger
8
- from sqlalchemy import text
13
+ from sqlalchemy import text, event
9
14
  from sqlalchemy.ext.asyncio import (
10
15
  create_async_engine,
11
16
  async_sessionmaker,
@@ -13,13 +18,14 @@ from sqlalchemy.ext.asyncio import (
13
18
  AsyncEngine,
14
19
  async_scoped_session,
15
20
  )
21
+ from sqlalchemy.pool import NullPool
16
22
 
17
- from basic_memory.models import Base
18
- from basic_memory.models.search import CREATE_SEARCH_INDEX
23
+ from basic_memory.repository.search_repository import SearchRepository
19
24
 
20
25
  # Module level state
21
26
  _engine: Optional[AsyncEngine] = None
22
27
  _session_maker: Optional[async_sessionmaker[AsyncSession]] = None
28
+ _migrations_completed: bool = False
23
29
 
24
30
 
25
31
  class DatabaseType(Enum):
@@ -35,7 +41,7 @@ class DatabaseType(Enum):
35
41
  logger.info("Using in-memory SQLite database")
36
42
  return "sqlite+aiosqlite://"
37
43
 
38
- return f"sqlite+aiosqlite:///{db_path}"
44
+ return f"sqlite+aiosqlite:///{db_path}" # pragma: no cover
39
45
 
40
46
 
41
47
  def get_scoped_session_factory(
@@ -69,60 +75,125 @@ async def scoped_session(
69
75
  await factory.remove()
70
76
 
71
77
 
72
- async def init_db() -> None:
73
- """Initialize database with required tables."""
74
- if _session_maker is None: # pragma: no cover
75
- raise RuntimeError("Database session maker not initialized")
78
+ def _configure_sqlite_connection(dbapi_conn, enable_wal: bool = True) -> None:
79
+ """Configure SQLite connection with WAL mode and optimizations.
76
80
 
77
- logger.info("Initializing database...")
81
+ Args:
82
+ dbapi_conn: Database API connection object
83
+ enable_wal: Whether to enable WAL mode (should be False for in-memory databases)
84
+ """
85
+ cursor = dbapi_conn.cursor()
86
+ try:
87
+ # Enable WAL mode for better concurrency (not supported for in-memory databases)
88
+ if enable_wal:
89
+ cursor.execute("PRAGMA journal_mode=WAL")
90
+ # Set busy timeout to handle locked databases
91
+ cursor.execute("PRAGMA busy_timeout=10000") # 10 seconds
92
+ # Optimize for performance
93
+ cursor.execute("PRAGMA synchronous=NORMAL")
94
+ cursor.execute("PRAGMA cache_size=-64000") # 64MB cache
95
+ cursor.execute("PRAGMA temp_store=MEMORY")
96
+ # Windows-specific optimizations
97
+ if os.name == "nt":
98
+ cursor.execute("PRAGMA locking_mode=NORMAL") # Ensure normal locking on Windows
99
+ except Exception as e:
100
+ # Log but don't fail - some PRAGMAs may not be supported
101
+ logger.warning(f"Failed to configure SQLite connection: {e}")
102
+ finally:
103
+ cursor.close()
78
104
 
79
- async with scoped_session(_session_maker) as session:
80
- await session.execute(text("PRAGMA foreign_keys=ON"))
81
- conn = await session.connection()
82
- await conn.run_sync(Base.metadata.create_all)
83
105
 
84
- # recreate search index
85
- await session.execute(CREATE_SEARCH_INDEX)
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}")
86
112
 
87
- await session.commit()
113
+ # Configure connection args with Windows-specific settings
114
+ connect_args: dict[str, bool | float | None] = {"check_same_thread": False}
115
+
116
+ # Add Windows-specific parameters to improve reliability
117
+ if os.name == "nt": # Windows
118
+ connect_args.update(
119
+ {
120
+ "timeout": 30.0, # Increase timeout to 30 seconds for Windows
121
+ "isolation_level": None, # Use autocommit mode
122
+ }
123
+ )
124
+ # Use NullPool for Windows filesystem databases to avoid connection pooling issues
125
+ # Important: Do NOT use NullPool for in-memory databases as it will destroy the database
126
+ # between connections
127
+ if db_type == DatabaseType.FILESYSTEM:
128
+ engine = create_async_engine(
129
+ db_url,
130
+ connect_args=connect_args,
131
+ poolclass=NullPool, # Disable connection pooling on Windows
132
+ echo=False,
133
+ )
134
+ else:
135
+ # In-memory databases need connection pooling to maintain state
136
+ engine = create_async_engine(db_url, connect_args=connect_args)
137
+ else:
138
+ engine = create_async_engine(db_url, connect_args=connect_args)
139
+
140
+ # Enable WAL mode for better concurrency and reliability
141
+ # Note: WAL mode is not supported for in-memory databases
142
+ enable_wal = db_type != DatabaseType.MEMORY
143
+
144
+ @event.listens_for(engine.sync_engine, "connect")
145
+ def enable_wal_mode(dbapi_conn, connection_record):
146
+ """Enable WAL mode on each connection."""
147
+ _configure_sqlite_connection(dbapi_conn, enable_wal=enable_wal)
148
+
149
+ session_maker = async_sessionmaker(engine, expire_on_commit=False)
150
+ return engine, session_maker
88
151
 
89
152
 
90
153
  async def get_or_create_db(
91
154
  db_path: Path,
92
155
  db_type: DatabaseType = DatabaseType.FILESYSTEM,
156
+ ensure_migrations: bool = True,
93
157
  ) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: # pragma: no cover
94
158
  """Get or create database engine and session maker."""
95
159
  global _engine, _session_maker
96
160
 
97
161
  if _engine is None:
98
- db_url = DatabaseType.get_db_url(db_path, db_type)
99
- logger.debug(f"Creating engine for db_url: {db_url}")
100
- _engine = create_async_engine(db_url, connect_args={"check_same_thread": False})
101
- _session_maker = async_sessionmaker(_engine, expire_on_commit=False)
162
+ _engine, _session_maker = _create_engine_and_session(db_path, db_type)
163
+
164
+ # Run migrations automatically unless explicitly disabled
165
+ if ensure_migrations:
166
+ app_config = ConfigManager().config
167
+ await run_migrations(app_config, db_type)
102
168
 
103
- # Initialize database
104
- await init_db()
169
+ # These checks should never fail since we just created the engine and session maker
170
+ # if they were None, but we'll check anyway for the type checker
171
+ if _engine is None:
172
+ logger.error("Failed to create database engine", db_path=str(db_path))
173
+ raise RuntimeError("Database engine initialization failed")
174
+
175
+ if _session_maker is None:
176
+ logger.error("Failed to create session maker", db_path=str(db_path))
177
+ raise RuntimeError("Session maker initialization failed")
105
178
 
106
- assert _engine is not None # for type checker
107
- assert _session_maker is not None # for type checker
108
179
  return _engine, _session_maker
109
180
 
110
181
 
111
182
  async def shutdown_db() -> None: # pragma: no cover
112
183
  """Clean up database connections."""
113
- global _engine, _session_maker
184
+ global _engine, _session_maker, _migrations_completed
114
185
 
115
186
  if _engine:
116
187
  await _engine.dispose()
117
188
  _engine = None
118
189
  _session_maker = None
190
+ _migrations_completed = False
119
191
 
120
192
 
121
193
  @asynccontextmanager
122
194
  async def engine_session_factory(
123
195
  db_path: Path,
124
196
  db_type: DatabaseType = DatabaseType.MEMORY,
125
- init: bool = True,
126
197
  ) -> AsyncGenerator[tuple[AsyncEngine, async_sessionmaker[AsyncSession]], None]:
127
198
  """Create engine and session factory.
128
199
 
@@ -130,23 +201,112 @@ async def engine_session_factory(
130
201
  for each test. For production use, use get_or_create_db() instead.
131
202
  """
132
203
 
133
- global _engine, _session_maker
204
+ global _engine, _session_maker, _migrations_completed
134
205
 
135
206
  db_url = DatabaseType.get_db_url(db_path, db_type)
136
207
  logger.debug(f"Creating engine for db_url: {db_url}")
137
208
 
138
- _engine = create_async_engine(db_url, connect_args={"check_same_thread": False})
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)
235
+
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
239
+
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)
244
+
139
245
  try:
140
246
  _session_maker = async_sessionmaker(_engine, expire_on_commit=False)
141
247
 
142
- if init:
143
- await init_db()
248
+ # Verify that engine and session maker are initialized
249
+ if _engine is None: # pragma: no cover
250
+ logger.error("Database engine is None in engine_session_factory")
251
+ raise RuntimeError("Database engine initialization failed")
252
+
253
+ if _session_maker is None: # pragma: no cover
254
+ logger.error("Session maker is None in engine_session_factory")
255
+ raise RuntimeError("Session maker initialization failed")
144
256
 
145
- assert _engine is not None # for type checker
146
- assert _session_maker is not None # for type checker
147
257
  yield _engine, _session_maker
148
258
  finally:
149
259
  if _engine:
150
260
  await _engine.dispose()
151
261
  _engine = None
152
262
  _session_maker = None
263
+ _migrations_completed = False
264
+
265
+
266
+ async def run_migrations(
267
+ app_config: BasicMemoryConfig, database_type=DatabaseType.FILESYSTEM, force: bool = False
268
+ ): # 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
276
+
277
+ logger.info("Running database migrations...")
278
+ try:
279
+ # Get the absolute path to the alembic directory relative to this file
280
+ alembic_dir = Path(__file__).parent / "alembic"
281
+ config = Config()
282
+
283
+ # Set required Alembic config options programmatically
284
+ config.set_main_option("script_location", str(alembic_dir))
285
+ config.set_main_option(
286
+ "file_template",
287
+ "%%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s",
288
+ )
289
+ config.set_main_option("timezone", "UTC")
290
+ 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
+ )
294
+
295
+ command.upgrade(config, "head")
296
+ logger.info("Migrations completed successfully")
297
+
298
+ # Get session maker - ensure we don't trigger recursive migration calls
299
+ if _session_maker is None:
300
+ _, session_maker = _create_engine_and_session(app_config.database_path, database_type)
301
+ else:
302
+ session_maker = _session_maker
303
+
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
310
+ except Exception as e: # pragma: no cover
311
+ logger.error(f"Error running migrations: {e}")
312
+ raise
basic_memory/deps.py CHANGED
@@ -1,49 +1,105 @@
1
1
  """Dependency injection functions for basic-memory services."""
2
2
 
3
3
  from typing import Annotated
4
+ from loguru import logger
4
5
 
5
- from fastapi import Depends
6
+ from fastapi import Depends, HTTPException, Path, status, Request
6
7
  from sqlalchemy.ext.asyncio import (
7
8
  AsyncSession,
8
9
  AsyncEngine,
9
10
  async_sessionmaker,
10
11
  )
12
+ import pathlib
11
13
 
12
14
  from basic_memory import db
13
- from basic_memory.config import ProjectConfig, config
15
+ from basic_memory.config import ProjectConfig, BasicMemoryConfig, ConfigManager
16
+ from basic_memory.importers import (
17
+ ChatGPTImporter,
18
+ ClaudeConversationsImporter,
19
+ ClaudeProjectsImporter,
20
+ MemoryJsonImporter,
21
+ )
14
22
  from basic_memory.markdown import EntityParser
15
23
  from basic_memory.markdown.markdown_processor import MarkdownProcessor
16
24
  from basic_memory.repository.entity_repository import EntityRepository
17
25
  from basic_memory.repository.observation_repository import ObservationRepository
26
+ from basic_memory.repository.project_repository import ProjectRepository
18
27
  from basic_memory.repository.relation_repository import RelationRepository
19
28
  from basic_memory.repository.search_repository import SearchRepository
20
- from basic_memory.services import (
21
- EntityService,
22
- )
29
+ from basic_memory.services import EntityService, ProjectService
23
30
  from basic_memory.services.context_service import ContextService
31
+ from basic_memory.services.directory_service import DirectoryService
24
32
  from basic_memory.services.file_service import FileService
25
33
  from basic_memory.services.link_resolver import LinkResolver
26
34
  from basic_memory.services.search_service import SearchService
35
+ from basic_memory.sync import SyncService
36
+ from basic_memory.utils import generate_permalink
37
+
38
+
39
+ def get_app_config() -> BasicMemoryConfig: # pragma: no cover
40
+ app_config = ConfigManager().config
41
+ return app_config
42
+
43
+
44
+ AppConfigDep = Annotated[BasicMemoryConfig, Depends(get_app_config)] # pragma: no cover
27
45
 
28
46
 
29
47
  ## project
30
48
 
31
49
 
32
- def get_project_config() -> ProjectConfig: # pragma: no cover
33
- return config
50
+ async def get_project_config(
51
+ project: "ProjectPathDep", project_repository: "ProjectRepositoryDep"
52
+ ) -> ProjectConfig: # pragma: no cover
53
+ """Get the current project referenced from request state.
34
54
 
55
+ Args:
56
+ request: The current request object
57
+ project_repository: Repository for project operations
35
58
 
36
- ProjectConfigDep = Annotated[ProjectConfig, Depends(get_project_config)] # pragma: no cover
59
+ Returns:
60
+ The resolved project config
61
+
62
+ Raises:
63
+ HTTPException: If project is not found
64
+ """
65
+ # Convert project name to permalink for lookup
66
+ project_permalink = generate_permalink(str(project))
67
+ project_obj = await project_repository.get_by_permalink(project_permalink)
68
+ if project_obj:
69
+ return ProjectConfig(name=project_obj.name, home=pathlib.Path(project_obj.path))
37
70
 
71
+ # Not found
72
+ raise HTTPException( # pragma: no cover
73
+ status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{project}' not found."
74
+ )
75
+
76
+
77
+ ProjectConfigDep = Annotated[ProjectConfig, Depends(get_project_config)] # pragma: no cover
38
78
 
39
79
  ## sqlalchemy
40
80
 
41
81
 
42
82
  async def get_engine_factory(
43
- project_config: ProjectConfigDep,
83
+ request: Request,
44
84
  ) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: # pragma: no cover
45
- """Get engine and session maker."""
46
- return await db.get_or_create_db(project_config.database_path)
85
+ """Get cached engine and session maker from app state.
86
+
87
+ For API requests, returns cached connections from app.state for optimal performance.
88
+ For non-API contexts (CLI), falls back to direct database connection.
89
+ """
90
+ # Try to get cached connections from app state (API context)
91
+ if (
92
+ hasattr(request, "app")
93
+ and hasattr(request.app.state, "engine")
94
+ and hasattr(request.app.state, "session_maker")
95
+ ):
96
+ return request.app.state.engine, request.app.state.session_maker
97
+
98
+ # Fallback for non-API contexts (CLI)
99
+ logger.debug("Using fallback database connection for non-API context")
100
+ app_config = get_app_config()
101
+ engine, session_maker = await db.get_or_create_db(app_config.database_path)
102
+ return engine, session_maker
47
103
 
48
104
 
49
105
  EngineFactoryDep = Annotated[
@@ -63,11 +119,70 @@ SessionMakerDep = Annotated[async_sessionmaker, Depends(get_session_maker)]
63
119
  ## repositories
64
120
 
65
121
 
122
+ async def get_project_repository(
123
+ session_maker: SessionMakerDep,
124
+ ) -> ProjectRepository:
125
+ """Get the project repository."""
126
+ return ProjectRepository(session_maker)
127
+
128
+
129
+ ProjectRepositoryDep = Annotated[ProjectRepository, Depends(get_project_repository)]
130
+ ProjectPathDep = Annotated[str, Path()] # Use Path dependency to extract from URL
131
+
132
+
133
+ async def get_project_id(
134
+ project_repository: ProjectRepositoryDep,
135
+ project: ProjectPathDep,
136
+ ) -> int:
137
+ """Get the current project ID from request state.
138
+
139
+ When using sub-applications with /{project} mounting, the project value
140
+ is stored in request.state by middleware.
141
+
142
+ Args:
143
+ request: The current request object
144
+ project_repository: Repository for project operations
145
+
146
+ Returns:
147
+ The resolved project ID
148
+
149
+ Raises:
150
+ HTTPException: If project is not found
151
+ """
152
+ # Convert project name to permalink for lookup
153
+ project_permalink = generate_permalink(str(project))
154
+ project_obj = await project_repository.get_by_permalink(project_permalink)
155
+ if project_obj:
156
+ return project_obj.id
157
+
158
+ # Try by name if permalink lookup fails
159
+ project_obj = await project_repository.get_by_name(str(project)) # pragma: no cover
160
+ if project_obj: # pragma: no cover
161
+ return project_obj.id
162
+
163
+ # Not found
164
+ raise HTTPException( # pragma: no cover
165
+ status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{project}' not found."
166
+ )
167
+
168
+
169
+ """
170
+ The project_id dependency is used in the following:
171
+ - EntityRepository
172
+ - ObservationRepository
173
+ - RelationRepository
174
+ - SearchRepository
175
+ - ProjectInfoRepository
176
+ """
177
+ ProjectIdDep = Annotated[int, Depends(get_project_id)]
178
+
179
+
66
180
  async def get_entity_repository(
67
181
  session_maker: SessionMakerDep,
182
+ project_id: ProjectIdDep,
68
183
  ) -> EntityRepository:
69
- """Create an EntityRepository instance."""
70
- return EntityRepository(session_maker)
184
+ """Create an EntityRepository instance for the current project."""
185
+ return EntityRepository(session_maker, project_id=project_id)
71
186
 
72
187
 
73
188
  EntityRepositoryDep = Annotated[EntityRepository, Depends(get_entity_repository)]
@@ -75,9 +190,10 @@ EntityRepositoryDep = Annotated[EntityRepository, Depends(get_entity_repository)
75
190
 
76
191
  async def get_observation_repository(
77
192
  session_maker: SessionMakerDep,
193
+ project_id: ProjectIdDep,
78
194
  ) -> ObservationRepository:
79
- """Create an ObservationRepository instance."""
80
- return ObservationRepository(session_maker)
195
+ """Create an ObservationRepository instance for the current project."""
196
+ return ObservationRepository(session_maker, project_id=project_id)
81
197
 
82
198
 
83
199
  ObservationRepositoryDep = Annotated[ObservationRepository, Depends(get_observation_repository)]
@@ -85,9 +201,10 @@ ObservationRepositoryDep = Annotated[ObservationRepository, Depends(get_observat
85
201
 
86
202
  async def get_relation_repository(
87
203
  session_maker: SessionMakerDep,
204
+ project_id: ProjectIdDep,
88
205
  ) -> RelationRepository:
89
- """Create a RelationRepository instance."""
90
- return RelationRepository(session_maker)
206
+ """Create a RelationRepository instance for the current project."""
207
+ return RelationRepository(session_maker, project_id=project_id)
91
208
 
92
209
 
93
210
  RelationRepositoryDep = Annotated[RelationRepository, Depends(get_relation_repository)]
@@ -95,14 +212,18 @@ RelationRepositoryDep = Annotated[RelationRepository, Depends(get_relation_repos
95
212
 
96
213
  async def get_search_repository(
97
214
  session_maker: SessionMakerDep,
215
+ project_id: ProjectIdDep,
98
216
  ) -> SearchRepository:
99
- """Create a SearchRepository instance."""
100
- return SearchRepository(session_maker)
217
+ """Create a SearchRepository instance for the current project."""
218
+ return SearchRepository(session_maker, project_id=project_id)
101
219
 
102
220
 
103
221
  SearchRepositoryDep = Annotated[SearchRepository, Depends(get_search_repository)]
104
222
 
105
223
 
224
+ # ProjectInfoRepository is deprecated and will be removed in a future version.
225
+ # Use ProjectRepository instead, which has the same functionality plus more project-specific operations.
226
+
106
227
  ## services
107
228
 
108
229
 
@@ -123,7 +244,12 @@ MarkdownProcessorDep = Annotated[MarkdownProcessor, Depends(get_markdown_process
123
244
  async def get_file_service(
124
245
  project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
125
246
  ) -> FileService:
126
- return FileService(project_config.home, markdown_processor)
247
+ logger.debug(
248
+ f"Creating FileService for project: {project_config.name}, base_path: {project_config.home}"
249
+ )
250
+ file_service = FileService(project_config.home, markdown_processor)
251
+ logger.debug(f"Created FileService for project: {file_service} ")
252
+ return file_service
127
253
 
128
254
 
129
255
  FileServiceDep = Annotated[FileService, Depends(get_file_service)]
@@ -136,6 +262,7 @@ async def get_entity_service(
136
262
  entity_parser: EntityParserDep,
137
263
  file_service: FileServiceDep,
138
264
  link_resolver: "LinkResolverDep",
265
+ app_config: AppConfigDep,
139
266
  ) -> EntityService:
140
267
  """Create EntityService with repository."""
141
268
  return EntityService(
@@ -145,6 +272,7 @@ async def get_entity_service(
145
272
  entity_parser=entity_parser,
146
273
  file_service=file_service,
147
274
  link_resolver=link_resolver,
275
+ app_config=app_config,
148
276
  )
149
277
 
150
278
 
@@ -173,9 +301,111 @@ LinkResolverDep = Annotated[LinkResolver, Depends(get_link_resolver)]
173
301
 
174
302
 
175
303
  async def get_context_service(
176
- search_repository: SearchRepositoryDep, entity_repository: EntityRepositoryDep
304
+ search_repository: SearchRepositoryDep,
305
+ entity_repository: EntityRepositoryDep,
306
+ observation_repository: ObservationRepositoryDep,
177
307
  ) -> ContextService:
178
- return ContextService(search_repository, entity_repository)
308
+ return ContextService(
309
+ search_repository=search_repository,
310
+ entity_repository=entity_repository,
311
+ observation_repository=observation_repository,
312
+ )
179
313
 
180
314
 
181
315
  ContextServiceDep = Annotated[ContextService, Depends(get_context_service)]
316
+
317
+
318
+ async def get_sync_service(
319
+ app_config: AppConfigDep,
320
+ entity_service: EntityServiceDep,
321
+ entity_parser: EntityParserDep,
322
+ entity_repository: EntityRepositoryDep,
323
+ relation_repository: RelationRepositoryDep,
324
+ project_repository: ProjectRepositoryDep,
325
+ search_service: SearchServiceDep,
326
+ file_service: FileServiceDep,
327
+ ) -> SyncService: # pragma: no cover
328
+ """
329
+
330
+ :rtype: object
331
+ """
332
+ return SyncService(
333
+ app_config=app_config,
334
+ entity_service=entity_service,
335
+ entity_parser=entity_parser,
336
+ entity_repository=entity_repository,
337
+ relation_repository=relation_repository,
338
+ project_repository=project_repository,
339
+ search_service=search_service,
340
+ file_service=file_service,
341
+ )
342
+
343
+
344
+ SyncServiceDep = Annotated[SyncService, Depends(get_sync_service)]
345
+
346
+
347
+ async def get_project_service(
348
+ project_repository: ProjectRepositoryDep,
349
+ ) -> ProjectService:
350
+ """Create ProjectService with repository."""
351
+ return ProjectService(repository=project_repository)
352
+
353
+
354
+ ProjectServiceDep = Annotated[ProjectService, Depends(get_project_service)]
355
+
356
+
357
+ async def get_directory_service(
358
+ entity_repository: EntityRepositoryDep,
359
+ ) -> DirectoryService:
360
+ """Create DirectoryService with dependencies."""
361
+ return DirectoryService(
362
+ entity_repository=entity_repository,
363
+ )
364
+
365
+
366
+ DirectoryServiceDep = Annotated[DirectoryService, Depends(get_directory_service)]
367
+
368
+
369
+ # Import
370
+
371
+
372
+ async def get_chatgpt_importer(
373
+ project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
374
+ ) -> ChatGPTImporter:
375
+ """Create ChatGPTImporter with dependencies."""
376
+ return ChatGPTImporter(project_config.home, markdown_processor)
377
+
378
+
379
+ ChatGPTImporterDep = Annotated[ChatGPTImporter, Depends(get_chatgpt_importer)]
380
+
381
+
382
+ async def get_claude_conversations_importer(
383
+ project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
384
+ ) -> ClaudeConversationsImporter:
385
+ """Create ChatGPTImporter with dependencies."""
386
+ return ClaudeConversationsImporter(project_config.home, markdown_processor)
387
+
388
+
389
+ ClaudeConversationsImporterDep = Annotated[
390
+ ClaudeConversationsImporter, Depends(get_claude_conversations_importer)
391
+ ]
392
+
393
+
394
+ async def get_claude_projects_importer(
395
+ project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
396
+ ) -> ClaudeProjectsImporter:
397
+ """Create ChatGPTImporter with dependencies."""
398
+ return ClaudeProjectsImporter(project_config.home, markdown_processor)
399
+
400
+
401
+ ClaudeProjectsImporterDep = Annotated[ClaudeProjectsImporter, Depends(get_claude_projects_importer)]
402
+
403
+
404
+ async def get_memory_json_importer(
405
+ project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
406
+ ) -> MemoryJsonImporter:
407
+ """Create ChatGPTImporter with dependencies."""
408
+ return MemoryJsonImporter(project_config.home, markdown_processor)
409
+
410
+
411
+ MemoryJsonImporterDep = Annotated[MemoryJsonImporter, Depends(get_memory_json_importer)]