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.
- basic_memory/__init__.py +7 -1
- basic_memory/alembic/env.py +1 -1
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -5
- basic_memory/api/app.py +43 -13
- basic_memory/api/routers/__init__.py +4 -2
- basic_memory/api/routers/directory_router.py +63 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +127 -38
- basic_memory/api/routers/management_router.py +78 -0
- basic_memory/api/routers/memory_router.py +4 -59
- basic_memory/api/routers/project_router.py +230 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/search_router.py +3 -21
- basic_memory/api/routers/utils.py +130 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/cli/app.py +20 -21
- basic_memory/cli/commands/__init__.py +2 -1
- basic_memory/cli/commands/auth.py +136 -0
- basic_memory/cli/commands/db.py +3 -3
- basic_memory/cli/commands/import_chatgpt.py +31 -207
- basic_memory/cli/commands/import_claude_conversations.py +16 -142
- basic_memory/cli/commands/import_claude_projects.py +33 -143
- basic_memory/cli/commands/import_memory_json.py +26 -83
- basic_memory/cli/commands/mcp.py +71 -18
- basic_memory/cli/commands/project.py +99 -67
- basic_memory/cli/commands/status.py +19 -9
- basic_memory/cli/commands/sync.py +44 -58
- basic_memory/cli/main.py +1 -5
- basic_memory/config.py +144 -88
- basic_memory/db.py +6 -4
- basic_memory/deps.py +227 -30
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +222 -0
- basic_memory/importers/claude_conversations_importer.py +172 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +93 -0
- basic_memory/importers/utils.py +58 -0
- basic_memory/markdown/entity_parser.py +5 -2
- basic_memory/mcp/auth_provider.py +270 -0
- basic_memory/mcp/external_auth_provider.py +321 -0
- basic_memory/mcp/project_session.py +103 -0
- basic_memory/mcp/prompts/continue_conversation.py +18 -68
- basic_memory/mcp/prompts/recent_activity.py +19 -3
- basic_memory/mcp/prompts/search.py +14 -140
- basic_memory/mcp/prompts/utils.py +3 -3
- basic_memory/mcp/{tools → resources}/project_info.py +6 -2
- basic_memory/mcp/server.py +82 -8
- basic_memory/mcp/supabase_auth_provider.py +463 -0
- basic_memory/mcp/tools/__init__.py +20 -0
- basic_memory/mcp/tools/build_context.py +11 -1
- basic_memory/mcp/tools/canvas.py +15 -2
- basic_memory/mcp/tools/delete_note.py +12 -4
- basic_memory/mcp/tools/edit_note.py +297 -0
- basic_memory/mcp/tools/list_directory.py +154 -0
- basic_memory/mcp/tools/move_note.py +87 -0
- basic_memory/mcp/tools/project_management.py +300 -0
- basic_memory/mcp/tools/read_content.py +15 -6
- basic_memory/mcp/tools/read_note.py +17 -5
- basic_memory/mcp/tools/recent_activity.py +11 -2
- basic_memory/mcp/tools/search.py +10 -1
- basic_memory/mcp/tools/utils.py +137 -12
- basic_memory/mcp/tools/write_note.py +11 -15
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +16 -4
- basic_memory/models/project.py +80 -0
- basic_memory/models/search.py +8 -5
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +8 -3
- basic_memory/repository/observation_repository.py +35 -3
- basic_memory/repository/project_info_repository.py +3 -2
- basic_memory/repository/project_repository.py +85 -0
- basic_memory/repository/relation_repository.py +8 -2
- basic_memory/repository/repository.py +107 -15
- basic_memory/repository/search_repository.py +87 -27
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +34 -0
- basic_memory/schemas/memory.py +26 -12
- basic_memory/schemas/project_info.py +112 -2
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +56 -2
- basic_memory/schemas/search.py +1 -1
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +208 -95
- basic_memory/services/directory_service.py +167 -0
- basic_memory/services/entity_service.py +385 -5
- basic_memory/services/exceptions.py +6 -0
- basic_memory/services/file_service.py +14 -15
- basic_memory/services/initialization.py +144 -67
- basic_memory/services/link_resolver.py +16 -8
- basic_memory/services/project_service.py +548 -0
- basic_memory/services/search_service.py +77 -2
- basic_memory/sync/background_sync.py +25 -0
- basic_memory/sync/sync_service.py +10 -9
- basic_memory/sync/watch_service.py +63 -39
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/METADATA +23 -1
- basic_memory-0.13.0b2.dist-info/RECORD +132 -0
- basic_memory/api/routers/project_info_router.py +0 -274
- basic_memory/mcp/main.py +0 -24
- basic_memory-0.12.3.dist-info/RECORD +0 -100
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/WHEEL +0 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
|
14
|
-
from basic_memory.
|
|
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:
|
|
39
|
-
)
|
|
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
|
-
|
|
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
|
-
#
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
config=app_config,
|
|
143
|
+
app_config=app_config,
|
|
144
|
+
project_repository=project_repository,
|
|
61
145
|
quiet=True,
|
|
62
146
|
)
|
|
63
147
|
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
83
|
-
)
|
|
180
|
+
app_config: BasicMemoryConfig,
|
|
181
|
+
):
|
|
84
182
|
"""Initialize the Basic Memory application.
|
|
85
183
|
|
|
86
|
-
This function handles all initialization steps
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
#
|
|
107
|
-
|
|
200
|
+
# migrate legacy project data
|
|
201
|
+
await migrate_legacy_projects(app_config)
|
|
108
202
|
|
|
109
203
|
|
|
110
|
-
def ensure_initialization(app_config:
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
19
|
-
3. Try exact
|
|
20
|
-
4.
|
|
21
|
-
5.
|
|
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.
|
|
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
|
-
#
|
|
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(
|
|
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.
|
|
73
|
+
logger.trace(
|
|
66
74
|
f"Selected best match from {len(results)} results: {best_match.permalink}"
|
|
67
75
|
)
|
|
68
76
|
if best_match.permalink:
|