basic-memory 0.12.3__py3-none-any.whl → 0.13.0b2__py3-none-any.whl

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

Potentially problematic release.


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

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 +144 -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.0b2.dist-info}/METADATA +23 -1
  101. basic_memory-0.13.0b2.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.0b2.dist-info}/WHEEL +0 -0
  106. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/entry_points.txt +0 -0
  107. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/licenses/LICENSE +0 -0
@@ -5,19 +5,18 @@ to ensure consistent application startup across all entry points.
5
5
  """
6
6
 
7
7
  import asyncio
8
- from typing import Optional
8
+ import shutil
9
+ from pathlib import Path
9
10
 
10
11
  from loguru import logger
11
12
 
12
13
  from basic_memory import db
13
- from basic_memory.config import ProjectConfig, config_manager
14
- from basic_memory.sync import WatchService
14
+ from basic_memory.config import BasicMemoryConfig
15
+ from basic_memory.models import Project
16
+ from basic_memory.repository import ProjectRepository
15
17
 
16
- # Import this inside functions to avoid circular imports
17
- # from basic_memory.cli.commands.sync import get_sync_service
18
18
 
19
-
20
- async def initialize_database(app_config: ProjectConfig) -> None:
19
+ async def initialize_database(app_config: BasicMemoryConfig) -> None:
21
20
  """Run database migrations to ensure schema is up to date.
22
21
 
23
22
  Args:
@@ -34,80 +33,175 @@ async def initialize_database(app_config: ProjectConfig) -> None:
34
33
  # more specific error if the database is actually unusable
35
34
 
36
35
 
36
+ async def reconcile_projects_with_config(app_config: BasicMemoryConfig):
37
+ """Ensure all projects in config.json exist in the projects table and vice versa.
38
+
39
+ This uses the ProjectService's synchronize_projects method to ensure bidirectional
40
+ synchronization between the configuration file and the database.
41
+
42
+ Args:
43
+ app_config: The Basic Memory application configuration
44
+ """
45
+ logger.info("Reconciling projects from config with database...")
46
+
47
+ # Get database session
48
+ _, session_maker = await db.get_or_create_db(
49
+ db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
50
+ )
51
+ project_repository = ProjectRepository(session_maker)
52
+
53
+ # Import ProjectService here to avoid circular imports
54
+ from basic_memory.services.project_service import ProjectService
55
+
56
+ try:
57
+ # Create project service and synchronize projects
58
+ project_service = ProjectService(repository=project_repository)
59
+ await project_service.synchronize_projects()
60
+ logger.info("Projects successfully reconciled between config and database")
61
+ except Exception as e:
62
+ # Log the error but continue with initialization
63
+ logger.error(f"Error during project synchronization: {e}")
64
+ logger.info("Continuing with initialization despite synchronization error")
65
+
66
+
67
+ async def migrate_legacy_projects(app_config: BasicMemoryConfig):
68
+ # Get database session
69
+ _, session_maker = await db.get_or_create_db(
70
+ db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
71
+ )
72
+ logger.info("Migrating legacy projects...")
73
+ project_repository = ProjectRepository(session_maker)
74
+
75
+ # For each project in config.json, check if it has a .basic-memory dir
76
+ for project_name, project_path in app_config.projects.items():
77
+ legacy_dir = Path(project_path) / ".basic-memory"
78
+ if not legacy_dir.exists():
79
+ continue
80
+ logger.info(f"Detected legacy project directory: {legacy_dir}")
81
+ project = await project_repository.get_by_name(project_name)
82
+ if not project: # pragma: no cover
83
+ logger.error(f"Project {project_name} not found in database, skipping migration")
84
+ continue
85
+
86
+ await migrate_legacy_project_data(project, legacy_dir)
87
+ logger.info("Legacy projects successfully migrated")
88
+
89
+
90
+ async def migrate_legacy_project_data(project: Project, legacy_dir: Path) -> bool:
91
+ """Check if project has legacy .basic-memory dir and migrate if needed.
92
+
93
+ Args:
94
+ project: The project to check and potentially migrate
95
+
96
+ Returns:
97
+ True if migration occurred, False otherwise
98
+ """
99
+
100
+ # avoid circular imports
101
+ from basic_memory.cli.commands.sync import get_sync_service
102
+
103
+ sync_service = await get_sync_service(project)
104
+ sync_dir = Path(project.path)
105
+
106
+ logger.info(f"Sync starting project: {project.name}")
107
+ await sync_service.sync(sync_dir)
108
+ logger.info(f"Sync completed successfully for project: {project.name}")
109
+
110
+ # After successful sync, remove the legacy directory
111
+ try:
112
+ logger.info(f"Removing legacy directory: {legacy_dir}")
113
+ shutil.rmtree(legacy_dir)
114
+ return True
115
+ except Exception as e:
116
+ logger.error(f"Error removing legacy directory: {e}")
117
+ return False
118
+
119
+
37
120
  async def initialize_file_sync(
38
- app_config: ProjectConfig,
39
- ) -> asyncio.Task:
40
- """Initialize file synchronization services.
121
+ app_config: BasicMemoryConfig,
122
+ ):
123
+ """Initialize file synchronization services. This function starts the watch service and does not return
41
124
 
42
125
  Args:
43
126
  app_config: The Basic Memory project configuration
44
127
 
45
128
  Returns:
46
- Tuple of (sync_service, watch_service, watch_task) if sync is enabled,
47
- or (None, None, None) if sync is disabled
129
+ The watch service task that's monitoring file changes
48
130
  """
49
- # Load app configuration
50
- # Import here to avoid circular imports
51
- from basic_memory.cli.commands.sync import get_sync_service
52
131
 
53
- # Initialize sync service
54
- sync_service = await get_sync_service()
132
+ # delay import
133
+ from basic_memory.sync import WatchService
134
+
135
+ # Load app configuration
136
+ _, session_maker = await db.get_or_create_db(
137
+ db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
138
+ )
139
+ project_repository = ProjectRepository(session_maker)
55
140
 
56
141
  # Initialize watch service
57
142
  watch_service = WatchService(
58
- sync_service=sync_service,
59
- file_service=sync_service.entity_service.file_service,
60
- config=app_config,
143
+ app_config=app_config,
144
+ project_repository=project_repository,
61
145
  quiet=True,
62
146
  )
63
147
 
64
- # Create the background task for running sync
65
- async def run_background_sync(): # pragma: no cover
66
- # Run initial full sync
67
- await sync_service.sync(app_config.home)
68
- logger.info("Sync completed successfully")
148
+ # Get active projects
149
+ active_projects = await project_repository.get_active_projects()
150
+
151
+ # First, sync all projects sequentially
152
+ for project in active_projects:
153
+ # avoid circular imports
154
+ from basic_memory.cli.commands.sync import get_sync_service
155
+
156
+ logger.info(f"Starting sync for project: {project.name}")
157
+ sync_service = await get_sync_service(project)
158
+ sync_dir = Path(project.path)
69
159
 
70
- # Start background sync task
71
- logger.info(f"Starting watch service to sync file changes in dir: {app_config.home}")
160
+ try:
161
+ await sync_service.sync(sync_dir)
162
+ logger.info(f"Sync completed successfully for project: {project.name}")
163
+ except Exception as e: # pragma: no cover
164
+ logger.error(f"Error syncing project {project.name}: {e}")
165
+ # Continue with other projects even if one fails
72
166
 
73
- # Start watching for changes
167
+ # Then start the watch service in the background
168
+ logger.info("Starting watch service for all projects")
169
+ # run the watch service
170
+ try:
74
171
  await watch_service.run()
172
+ logger.info("Watch service started")
173
+ except Exception as e: # pragma: no cover
174
+ logger.error(f"Error starting watch service: {e}")
75
175
 
76
- watch_task = asyncio.create_task(run_background_sync())
77
- logger.info("Watch service started")
78
- return watch_task
176
+ return None
79
177
 
80
178
 
81
179
  async def initialize_app(
82
- app_config: ProjectConfig,
83
- ) -> Optional[asyncio.Task]:
180
+ app_config: BasicMemoryConfig,
181
+ ):
84
182
  """Initialize the Basic Memory application.
85
183
 
86
- This function handles all initialization steps needed for both API and shor lived CLI commands.
87
- For long running commands like mcp, a
184
+ This function handles all initialization steps:
88
185
  - Running database migrations
186
+ - Reconciling projects from config.json with projects table
89
187
  - Setting up file synchronization
188
+ - Migrating legacy project data
90
189
 
91
190
  Args:
92
191
  app_config: The Basic Memory project configuration
93
192
  """
193
+ logger.info("Initializing app...")
94
194
  # Initialize database first
95
195
  await initialize_database(app_config)
96
196
 
97
- basic_memory_config = config_manager.load_config()
98
- logger.info(f"Sync changes enabled: {basic_memory_config.sync_changes}")
99
- logger.info(
100
- f"Update permalinks on move enabled: {basic_memory_config.update_permalinks_on_move}"
101
- )
102
- if not basic_memory_config.sync_changes: # pragma: no cover
103
- logger.info("Sync changes disabled. Skipping watch service.")
104
- return
197
+ # Reconcile projects from config.json with projects table
198
+ await reconcile_projects_with_config(app_config)
105
199
 
106
- # Initialize file sync services
107
- return await initialize_file_sync(app_config)
200
+ # migrate legacy project data
201
+ await migrate_legacy_projects(app_config)
108
202
 
109
203
 
110
- def ensure_initialization(app_config: ProjectConfig) -> None:
204
+ def ensure_initialization(app_config: BasicMemoryConfig) -> None:
111
205
  """Ensure initialization runs in a synchronous context.
112
206
 
113
207
  This is a wrapper for the async initialize_app function that can be
@@ -117,27 +211,10 @@ def ensure_initialization(app_config: ProjectConfig) -> None:
117
211
  app_config: The Basic Memory project configuration
118
212
  """
119
213
  try:
120
- asyncio.run(initialize_app(app_config))
121
- except Exception as e:
122
- logger.error(f"Error during initialization: {e}")
123
- # Continue execution even if initialization fails
124
- # The command might still work, or will fail with a
125
- # more specific error message
126
-
127
-
128
- def ensure_initialize_database(app_config: ProjectConfig) -> None:
129
- """Ensure initialization runs in a synchronous context.
130
-
131
- This is a wrapper for the async initialize_database function that can be
132
- called from synchronous code like CLI entry points.
133
-
134
- Args:
135
- app_config: The Basic Memory project configuration
136
- """
137
- try:
138
- asyncio.run(initialize_database(app_config))
139
- except Exception as e:
140
- logger.error(f"Error during initialization: {e}")
214
+ result = asyncio.run(initialize_app(app_config))
215
+ logger.info(f"Initialization completed successfully: result={result}")
216
+ except Exception as e: # pragma: no cover
217
+ logger.exception(f"Error during initialization: {e}")
141
218
  # Continue execution even if initialization fails
142
219
  # The command might still work, or will fail with a
143
220
  # more specific error message
@@ -15,10 +15,10 @@ class LinkResolver:
15
15
 
16
16
  Uses a combination of exact matching and search-based resolution:
17
17
  1. Try exact permalink match (fastest)
18
- 2. Try permalink pattern match (for wildcards)
19
- 3. Try exact title match
20
- 4. Fall back to search for fuzzy matching
21
- 5. Generate new permalink if no match found
18
+ 2. Try exact title match
19
+ 3. Try exact file path match
20
+ 4. Try file path with .md extension (for folder/title patterns)
21
+ 5. Fall back to search for fuzzy matching
22
22
  """
23
23
 
24
24
  def __init__(self, entity_repository: EntityRepository, search_service: SearchService):
@@ -28,7 +28,7 @@ class LinkResolver:
28
28
 
29
29
  async def resolve_link(self, link_text: str, use_search: bool = True) -> Optional[Entity]:
30
30
  """Resolve a markdown link to a permalink."""
31
- logger.debug(f"Resolving link: {link_text}")
31
+ logger.trace(f"Resolving link: {link_text}")
32
32
 
33
33
  # Clean link text and extract any alias
34
34
  clean_text, alias = self._normalize_link_text(link_text)
@@ -52,17 +52,25 @@ class LinkResolver:
52
52
  logger.debug(f"Found entity with path: {found_path.file_path}")
53
53
  return found_path
54
54
 
55
+ # 4. Try file path with .md extension if not already present
56
+ if not clean_text.endswith(".md") and "/" in clean_text:
57
+ file_path_with_md = f"{clean_text}.md"
58
+ found_path_md = await self.entity_repository.get_by_file_path(file_path_with_md)
59
+ if found_path_md:
60
+ logger.debug(f"Found entity with path (with .md): {found_path_md.file_path}")
61
+ return found_path_md
62
+
55
63
  # search if indicated
56
64
  if use_search and "*" not in clean_text:
57
- # 3. Fall back to search for fuzzy matching on title
65
+ # 5. Fall back to search for fuzzy matching on title (use text search for prefix matching)
58
66
  results = await self.search_service.search(
59
- query=SearchQuery(title=clean_text, entity_types=[SearchItemType.ENTITY]),
67
+ query=SearchQuery(text=clean_text, entity_types=[SearchItemType.ENTITY]),
60
68
  )
61
69
 
62
70
  if results:
63
71
  # Look for best match
64
72
  best_match = min(results, key=lambda x: x.score) # pyright: ignore
65
- logger.debug(
73
+ logger.trace(
66
74
  f"Selected best match from {len(results)} results: {best_match.permalink}"
67
75
  )
68
76
  if best_match.permalink: