basic-memory 0.13.7.dev1__py3-none-any.whl → 0.14.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of basic-memory might be problematic. Click here for more details.
- basic_memory/__init__.py +1 -1
- basic_memory/api/routers/utils.py +1 -1
- basic_memory/cli/commands/db.py +8 -1
- basic_memory/cli/commands/mcp.py +1 -0
- basic_memory/cli/commands/project.py +3 -7
- basic_memory/config.py +6 -2
- basic_memory/db.py +5 -4
- basic_memory/mcp/project_session.py +16 -1
- basic_memory/mcp/prompts/sync_status.py +0 -4
- basic_memory/mcp/server.py +0 -1
- basic_memory/mcp/tools/build_context.py +6 -3
- basic_memory/mcp/tools/move_note.py +155 -1
- basic_memory/mcp/tools/read_note.py +6 -3
- basic_memory/mcp/tools/utils.py +27 -4
- basic_memory/mcp/tools/write_note.py +6 -2
- basic_memory/repository/entity_repository.py +46 -43
- basic_memory/repository/search_repository.py +153 -23
- basic_memory/schemas/memory.py +1 -1
- basic_memory/services/entity_service.py +10 -5
- basic_memory/services/initialization.py +11 -5
- basic_memory/services/project_service.py +18 -0
- basic_memory/services/sync_status_service.py +17 -0
- {basic_memory-0.13.7.dev1.dist-info → basic_memory-0.14.0b1.dist-info}/METADATA +26 -1
- {basic_memory-0.13.7.dev1.dist-info → basic_memory-0.14.0b1.dist-info}/RECORD +27 -27
- {basic_memory-0.13.7.dev1.dist-info → basic_memory-0.14.0b1.dist-info}/WHEEL +0 -0
- {basic_memory-0.13.7.dev1.dist-info → basic_memory-0.14.0b1.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.13.7.dev1.dist-info → basic_memory-0.14.0b1.dist-info}/licenses/LICENSE +0 -0
basic_memory/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
|
|
2
2
|
|
|
3
3
|
# Package version - updated by release automation
|
|
4
|
-
__version__ = "0.
|
|
4
|
+
__version__ = "0.14.0b1"
|
|
5
5
|
|
|
6
6
|
# API version for FastAPI - independent of package version
|
|
7
7
|
__api_version__ = "v0"
|
|
@@ -52,7 +52,7 @@ async def to_graph_context(
|
|
|
52
52
|
file_path=item.file_path,
|
|
53
53
|
permalink=item.permalink, # pyright: ignore
|
|
54
54
|
relation_type=item.relation_type, # pyright: ignore
|
|
55
|
-
from_entity=from_entity.title
|
|
55
|
+
from_entity=from_entity.title if from_entity else None,
|
|
56
56
|
to_entity=to_entity.title if to_entity else None,
|
|
57
57
|
created_at=item.created_at,
|
|
58
58
|
)
|
basic_memory/cli/commands/db.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"""Database management commands."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import typer
|
|
6
7
|
from loguru import logger
|
|
7
8
|
|
|
8
9
|
from basic_memory import db
|
|
9
10
|
from basic_memory.cli.app import app
|
|
10
|
-
from basic_memory.config import app_config
|
|
11
|
+
from basic_memory.config import app_config, config_manager
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
@app.command()
|
|
@@ -25,6 +26,12 @@ def reset(
|
|
|
25
26
|
db_path.unlink()
|
|
26
27
|
logger.info(f"Database file deleted: {db_path}")
|
|
27
28
|
|
|
29
|
+
# Reset project configuration
|
|
30
|
+
config_manager.config.projects = {"main": str(Path.home() / "basic-memory")}
|
|
31
|
+
config_manager.config.default_project = "main"
|
|
32
|
+
config_manager.save_config(config_manager.config)
|
|
33
|
+
logger.info("Project configuration reset to default")
|
|
34
|
+
|
|
28
35
|
# Create a new empty database
|
|
29
36
|
asyncio.run(db.run_migrations(app_config))
|
|
30
37
|
logger.info("Database reset complete")
|
basic_memory/cli/commands/mcp.py
CHANGED
|
@@ -120,7 +120,7 @@ def set_default_project(
|
|
|
120
120
|
try:
|
|
121
121
|
project_name = generate_permalink(name)
|
|
122
122
|
|
|
123
|
-
response = asyncio.run(call_put(client, f"projects/{project_name}/default"))
|
|
123
|
+
response = asyncio.run(call_put(client, f"/projects/{project_name}/default"))
|
|
124
124
|
result = ProjectStatusResponse.model_validate(response.json())
|
|
125
125
|
|
|
126
126
|
console.print(f"[green]{result.message}[/green]")
|
|
@@ -128,12 +128,8 @@ def set_default_project(
|
|
|
128
128
|
console.print(f"[red]Error setting default project: {str(e)}[/red]")
|
|
129
129
|
raise typer.Exit(1)
|
|
130
130
|
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
from basic_memory import config as config_module
|
|
134
|
-
|
|
135
|
-
reload(config_module)
|
|
136
|
-
|
|
131
|
+
# The API call above should have updated both config and MCP session
|
|
132
|
+
# No need for manual reload - the project service handles this automatically
|
|
137
133
|
console.print("[green]Project activated for current session[/green]")
|
|
138
134
|
|
|
139
135
|
|
basic_memory/config.py
CHANGED
|
@@ -45,7 +45,9 @@ class BasicMemoryConfig(BaseSettings):
|
|
|
45
45
|
env: Environment = Field(default="dev", description="Environment name")
|
|
46
46
|
|
|
47
47
|
projects: Dict[str, str] = Field(
|
|
48
|
-
default_factory=lambda: {
|
|
48
|
+
default_factory=lambda: {
|
|
49
|
+
"main": str(Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory")))
|
|
50
|
+
},
|
|
49
51
|
description="Mapping of project names to their filesystem paths",
|
|
50
52
|
)
|
|
51
53
|
default_project: str = Field(
|
|
@@ -92,7 +94,9 @@ class BasicMemoryConfig(BaseSettings):
|
|
|
92
94
|
"""Ensure configuration is valid after initialization."""
|
|
93
95
|
# Ensure main project exists
|
|
94
96
|
if "main" not in self.projects: # pragma: no cover
|
|
95
|
-
self.projects["main"] = str(
|
|
97
|
+
self.projects["main"] = str(
|
|
98
|
+
Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory"))
|
|
99
|
+
)
|
|
96
100
|
|
|
97
101
|
# Ensure default project is valid
|
|
98
102
|
if self.default_project not in self.projects: # pragma: no cover
|
basic_memory/db.py
CHANGED
|
@@ -95,11 +95,12 @@ async def get_or_create_db(
|
|
|
95
95
|
|
|
96
96
|
if _engine is None:
|
|
97
97
|
_engine, _session_maker = _create_engine_and_session(db_path, db_type)
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
# Run migrations automatically unless explicitly disabled
|
|
100
100
|
if ensure_migrations:
|
|
101
101
|
if app_config is None:
|
|
102
102
|
from basic_memory.config import app_config as global_app_config
|
|
103
|
+
|
|
103
104
|
app_config = global_app_config
|
|
104
105
|
await run_migrations(app_config, db_type)
|
|
105
106
|
|
|
@@ -170,12 +171,12 @@ async def run_migrations(
|
|
|
170
171
|
): # pragma: no cover
|
|
171
172
|
"""Run any pending alembic migrations."""
|
|
172
173
|
global _migrations_completed
|
|
173
|
-
|
|
174
|
+
|
|
174
175
|
# Skip if migrations already completed unless forced
|
|
175
176
|
if _migrations_completed and not force:
|
|
176
177
|
logger.debug("Migrations already completed in this session, skipping")
|
|
177
178
|
return
|
|
178
|
-
|
|
179
|
+
|
|
179
180
|
logger.info("Running database migrations...")
|
|
180
181
|
try:
|
|
181
182
|
# Get the absolute path to the alembic directory relative to this file
|
|
@@ -206,7 +207,7 @@ async def run_migrations(
|
|
|
206
207
|
# initialize the search Index schema
|
|
207
208
|
# the project_id is not used for init_search_index, so we pass a dummy value
|
|
208
209
|
await SearchRepository(session_maker, 1).init_search_index()
|
|
209
|
-
|
|
210
|
+
|
|
210
211
|
# Mark migrations as completed
|
|
211
212
|
_migrations_completed = True
|
|
212
213
|
except Exception as e: # pragma: no cover
|
|
@@ -8,7 +8,7 @@ from dataclasses import dataclass
|
|
|
8
8
|
from typing import Optional
|
|
9
9
|
from loguru import logger
|
|
10
10
|
|
|
11
|
-
from basic_memory.config import ProjectConfig, get_project_config
|
|
11
|
+
from basic_memory.config import ProjectConfig, get_project_config, config_manager
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@dataclass
|
|
@@ -64,6 +64,21 @@ class ProjectSession:
|
|
|
64
64
|
self.current_project = self.default_project # pragma: no cover
|
|
65
65
|
logger.info(f"Reset project context to default: {self.default_project}") # pragma: no cover
|
|
66
66
|
|
|
67
|
+
def refresh_from_config(self) -> None:
|
|
68
|
+
"""Refresh session state from current configuration.
|
|
69
|
+
|
|
70
|
+
This method reloads the default project from config and reinitializes
|
|
71
|
+
the session. This should be called when the default project is changed
|
|
72
|
+
via CLI or API to ensure MCP session stays in sync.
|
|
73
|
+
"""
|
|
74
|
+
# Reload config to get latest default project
|
|
75
|
+
current_config = config_manager.load_config()
|
|
76
|
+
new_default = current_config.default_project
|
|
77
|
+
|
|
78
|
+
# Reinitialize with new default
|
|
79
|
+
self.initialize(new_default)
|
|
80
|
+
logger.info(f"Refreshed project session from config, new default: {new_default}")
|
|
81
|
+
|
|
67
82
|
|
|
68
83
|
# Global session instance
|
|
69
84
|
session = ProjectSession()
|
|
@@ -12,10 +12,6 @@ from basic_memory.mcp.server import mcp
|
|
|
12
12
|
)
|
|
13
13
|
async def sync_status_prompt() -> str:
|
|
14
14
|
"""Get sync status with AI assistant guidance.
|
|
15
|
-
|
|
16
|
-
This prompt provides detailed sync status information along with
|
|
17
|
-
recommendations for how AI assistants should handle different sync states.
|
|
18
|
-
|
|
19
15
|
Returns:
|
|
20
16
|
Formatted sync status with AI assistant guidance
|
|
21
17
|
"""
|
basic_memory/mcp/server.py
CHANGED
|
@@ -82,10 +82,15 @@ async def build_context(
|
|
|
82
82
|
logger.info(f"Building context from {url}")
|
|
83
83
|
# URL is already validated and normalized by MemoryUrl type annotation
|
|
84
84
|
|
|
85
|
+
# Get the active project first to check project-specific sync status
|
|
86
|
+
active_project = get_active_project(project)
|
|
87
|
+
|
|
85
88
|
# Check migration status and wait briefly if needed
|
|
86
89
|
from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
|
|
87
90
|
|
|
88
|
-
migration_status = await wait_for_migration_or_return_status(
|
|
91
|
+
migration_status = await wait_for_migration_or_return_status(
|
|
92
|
+
timeout=5.0, project_name=active_project.name
|
|
93
|
+
)
|
|
89
94
|
if migration_status: # pragma: no cover
|
|
90
95
|
# Return a proper GraphContext with status message
|
|
91
96
|
from basic_memory.schemas.memory import MemoryMetadata
|
|
@@ -102,8 +107,6 @@ async def build_context(
|
|
|
102
107
|
uri=migration_status, # Include status in metadata
|
|
103
108
|
),
|
|
104
109
|
)
|
|
105
|
-
|
|
106
|
-
active_project = get_active_project(project)
|
|
107
110
|
project_url = active_project.project_url
|
|
108
111
|
|
|
109
112
|
response = await call_get(
|
|
@@ -7,9 +7,155 @@ from loguru import logger
|
|
|
7
7
|
|
|
8
8
|
from basic_memory.mcp.async_client import client
|
|
9
9
|
from basic_memory.mcp.server import mcp
|
|
10
|
-
from basic_memory.mcp.tools.utils import call_post
|
|
10
|
+
from basic_memory.mcp.tools.utils import call_post, call_get
|
|
11
11
|
from basic_memory.mcp.project_session import get_active_project
|
|
12
12
|
from basic_memory.schemas import EntityResponse
|
|
13
|
+
from basic_memory.schemas.project_info import ProjectList
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def _detect_cross_project_move_attempt(
|
|
17
|
+
identifier: str, destination_path: str, current_project: str
|
|
18
|
+
) -> Optional[str]:
|
|
19
|
+
"""Detect potential cross-project move attempts and return guidance.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
identifier: The note identifier being moved
|
|
23
|
+
destination_path: The destination path
|
|
24
|
+
current_project: The current active project
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Error message with guidance if cross-project move is detected, None otherwise
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
# Get list of all available projects to check against
|
|
31
|
+
response = await call_get(client, "/projects/projects")
|
|
32
|
+
project_list = ProjectList.model_validate(response.json())
|
|
33
|
+
project_names = [p.name.lower() for p in project_list.projects]
|
|
34
|
+
|
|
35
|
+
# Check if destination path contains any project names
|
|
36
|
+
dest_lower = destination_path.lower()
|
|
37
|
+
path_parts = dest_lower.split("/")
|
|
38
|
+
|
|
39
|
+
# Look for project names in the destination path
|
|
40
|
+
for part in path_parts:
|
|
41
|
+
if part in project_names and part != current_project.lower():
|
|
42
|
+
# Found a different project name in the path
|
|
43
|
+
matching_project = next(
|
|
44
|
+
p.name for p in project_list.projects if p.name.lower() == part
|
|
45
|
+
)
|
|
46
|
+
return _format_cross_project_error_response(
|
|
47
|
+
identifier, destination_path, current_project, matching_project
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Check if the destination path looks like it might be trying to reference another project
|
|
51
|
+
# (e.g., contains common project-like patterns)
|
|
52
|
+
if any(keyword in dest_lower for keyword in ["project", "workspace", "repo"]):
|
|
53
|
+
# This might be a cross-project attempt, but we can't be sure
|
|
54
|
+
# Return a general guidance message
|
|
55
|
+
available_projects = [
|
|
56
|
+
p.name for p in project_list.projects if p.name != current_project
|
|
57
|
+
]
|
|
58
|
+
if available_projects:
|
|
59
|
+
return _format_potential_cross_project_guidance(
|
|
60
|
+
identifier, destination_path, current_project, available_projects
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
# If we can't detect, don't interfere with normal error handling
|
|
65
|
+
logger.debug(f"Could not check for cross-project move: {e}")
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _format_cross_project_error_response(
|
|
72
|
+
identifier: str, destination_path: str, current_project: str, target_project: str
|
|
73
|
+
) -> str:
|
|
74
|
+
"""Format error response for detected cross-project move attempts."""
|
|
75
|
+
return dedent(f"""
|
|
76
|
+
# Move Failed - Cross-Project Move Not Supported
|
|
77
|
+
|
|
78
|
+
Cannot move '{identifier}' to '{destination_path}' because it appears to reference a different project ('{target_project}').
|
|
79
|
+
|
|
80
|
+
**Current project:** {current_project}
|
|
81
|
+
**Target project:** {target_project}
|
|
82
|
+
|
|
83
|
+
## Cross-project moves are not supported directly
|
|
84
|
+
|
|
85
|
+
Notes can only be moved within the same project. To move content between projects, use this workflow:
|
|
86
|
+
|
|
87
|
+
### Recommended approach:
|
|
88
|
+
```
|
|
89
|
+
# 1. Read the note content from current project
|
|
90
|
+
read_note("{identifier}")
|
|
91
|
+
|
|
92
|
+
# 2. Switch to the target project
|
|
93
|
+
switch_project("{target_project}")
|
|
94
|
+
|
|
95
|
+
# 3. Create the note in the target project
|
|
96
|
+
write_note("Note Title", "content from step 1", "target-folder")
|
|
97
|
+
|
|
98
|
+
# 4. Switch back to original project (optional)
|
|
99
|
+
switch_project("{current_project}")
|
|
100
|
+
|
|
101
|
+
# 5. Delete the original note if desired
|
|
102
|
+
delete_note("{identifier}")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Alternative: Stay in current project
|
|
106
|
+
If you want to move the note within the **{current_project}** project only:
|
|
107
|
+
```
|
|
108
|
+
move_note("{identifier}", "new-folder/new-name.md")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Available projects:
|
|
112
|
+
Use `list_projects()` to see all available projects and `switch_project("project-name")` to change projects.
|
|
113
|
+
""").strip()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _format_potential_cross_project_guidance(
|
|
117
|
+
identifier: str, destination_path: str, current_project: str, available_projects: list[str]
|
|
118
|
+
) -> str:
|
|
119
|
+
"""Format guidance for potentially cross-project moves."""
|
|
120
|
+
other_projects = ", ".join(available_projects[:3]) # Show first 3 projects
|
|
121
|
+
if len(available_projects) > 3:
|
|
122
|
+
other_projects += f" (and {len(available_projects) - 3} others)"
|
|
123
|
+
|
|
124
|
+
return dedent(f"""
|
|
125
|
+
# Move Failed - Check Project Context
|
|
126
|
+
|
|
127
|
+
Cannot move '{identifier}' to '{destination_path}' within the current project '{current_project}'.
|
|
128
|
+
|
|
129
|
+
## If you intended to move within the current project:
|
|
130
|
+
The destination path should be relative to the project root:
|
|
131
|
+
```
|
|
132
|
+
move_note("{identifier}", "folder/filename.md")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## If you intended to move to a different project:
|
|
136
|
+
Cross-project moves require switching projects first. Available projects: {other_projects}
|
|
137
|
+
|
|
138
|
+
### To move to another project:
|
|
139
|
+
```
|
|
140
|
+
# 1. Read the content
|
|
141
|
+
read_note("{identifier}")
|
|
142
|
+
|
|
143
|
+
# 2. Switch to target project
|
|
144
|
+
switch_project("target-project-name")
|
|
145
|
+
|
|
146
|
+
# 3. Create note in target project
|
|
147
|
+
write_note("Title", "content", "folder")
|
|
148
|
+
|
|
149
|
+
# 4. Switch back and delete original if desired
|
|
150
|
+
switch_project("{current_project}")
|
|
151
|
+
delete_note("{identifier}")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### To see all projects:
|
|
155
|
+
```
|
|
156
|
+
list_projects()
|
|
157
|
+
```
|
|
158
|
+
""").strip()
|
|
13
159
|
|
|
14
160
|
|
|
15
161
|
def _format_move_error_response(error_message: str, identifier: str, destination_path: str) -> str:
|
|
@@ -258,6 +404,14 @@ async def move_note(
|
|
|
258
404
|
active_project = get_active_project(project)
|
|
259
405
|
project_url = active_project.project_url
|
|
260
406
|
|
|
407
|
+
# Check for potential cross-project move attempts
|
|
408
|
+
cross_project_error = await _detect_cross_project_move_attempt(
|
|
409
|
+
identifier, destination_path, active_project.name
|
|
410
|
+
)
|
|
411
|
+
if cross_project_error:
|
|
412
|
+
logger.info(f"Detected cross-project move attempt: {identifier} -> {destination_path}")
|
|
413
|
+
return cross_project_error
|
|
414
|
+
|
|
261
415
|
try:
|
|
262
416
|
# Prepare move request
|
|
263
417
|
move_data = {
|
|
@@ -52,14 +52,17 @@ async def read_note(
|
|
|
52
52
|
read_note("Meeting Notes", project="work-project")
|
|
53
53
|
"""
|
|
54
54
|
|
|
55
|
+
# Get the active project first to check project-specific sync status
|
|
56
|
+
active_project = get_active_project(project)
|
|
57
|
+
|
|
55
58
|
# Check migration status and wait briefly if needed
|
|
56
59
|
from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
|
|
57
60
|
|
|
58
|
-
migration_status = await wait_for_migration_or_return_status(
|
|
61
|
+
migration_status = await wait_for_migration_or_return_status(
|
|
62
|
+
timeout=5.0, project_name=active_project.name
|
|
63
|
+
)
|
|
59
64
|
if migration_status: # pragma: no cover
|
|
60
65
|
return f"# System Status\n\n{migration_status}\n\nPlease wait for migration to complete before reading notes."
|
|
61
|
-
|
|
62
|
-
active_project = get_active_project(project)
|
|
63
66
|
project_url = active_project.project_url
|
|
64
67
|
|
|
65
68
|
# Get the file via REST API - first try direct permalink lookup
|
basic_memory/mcp/tools/utils.py
CHANGED
|
@@ -525,11 +525,16 @@ def check_migration_status() -> Optional[str]:
|
|
|
525
525
|
return None
|
|
526
526
|
|
|
527
527
|
|
|
528
|
-
async def wait_for_migration_or_return_status(
|
|
528
|
+
async def wait_for_migration_or_return_status(
|
|
529
|
+
timeout: float = 5.0, project_name: Optional[str] = None
|
|
530
|
+
) -> Optional[str]:
|
|
529
531
|
"""Wait briefly for sync/migration to complete, or return status message.
|
|
530
532
|
|
|
531
533
|
Args:
|
|
532
534
|
timeout: Maximum time to wait for sync completion
|
|
535
|
+
project_name: Optional project name to check specific project status.
|
|
536
|
+
If provided, only checks that project's readiness.
|
|
537
|
+
If None, uses global status check (legacy behavior).
|
|
533
538
|
|
|
534
539
|
Returns:
|
|
535
540
|
Status message if sync is still in progress, None if ready
|
|
@@ -538,18 +543,36 @@ async def wait_for_migration_or_return_status(timeout: float = 5.0) -> Optional[
|
|
|
538
543
|
from basic_memory.services.sync_status_service import sync_status_tracker
|
|
539
544
|
import asyncio
|
|
540
545
|
|
|
541
|
-
if
|
|
546
|
+
# Check if we should use project-specific or global status
|
|
547
|
+
def is_ready() -> bool:
|
|
548
|
+
if project_name:
|
|
549
|
+
return sync_status_tracker.is_project_ready(project_name)
|
|
550
|
+
return sync_status_tracker.is_ready
|
|
551
|
+
|
|
552
|
+
if is_ready():
|
|
542
553
|
return None
|
|
543
554
|
|
|
544
555
|
# Wait briefly for sync to complete
|
|
545
556
|
start_time = asyncio.get_event_loop().time()
|
|
546
557
|
while (asyncio.get_event_loop().time() - start_time) < timeout:
|
|
547
|
-
if
|
|
558
|
+
if is_ready():
|
|
548
559
|
return None
|
|
549
560
|
await asyncio.sleep(0.1) # Check every 100ms
|
|
550
561
|
|
|
551
562
|
# Still not ready after timeout
|
|
552
|
-
|
|
563
|
+
if project_name:
|
|
564
|
+
# For project-specific checks, get project status details
|
|
565
|
+
project_status = sync_status_tracker.get_project_status(project_name)
|
|
566
|
+
if project_status and project_status.status.value == "failed":
|
|
567
|
+
error_msg = project_status.error or "Unknown sync error"
|
|
568
|
+
return f"❌ Sync failed for project '{project_name}': {error_msg}"
|
|
569
|
+
elif project_status:
|
|
570
|
+
return f"🔄 Project '{project_name}' is still syncing: {project_status.message}"
|
|
571
|
+
else:
|
|
572
|
+
return f"⚠️ Project '{project_name}' status unknown"
|
|
573
|
+
else:
|
|
574
|
+
# Fall back to global summary for legacy calls
|
|
575
|
+
return sync_status_tracker.get_summary()
|
|
553
576
|
except Exception: # pragma: no cover
|
|
554
577
|
# If there's any error, assume ready
|
|
555
578
|
return None
|
|
@@ -72,10 +72,15 @@ async def write_note(
|
|
|
72
72
|
"""
|
|
73
73
|
logger.info(f"MCP tool call tool=write_note folder={folder}, title={title}, tags={tags}")
|
|
74
74
|
|
|
75
|
+
# Get the active project first to check project-specific sync status
|
|
76
|
+
active_project = get_active_project(project)
|
|
77
|
+
|
|
75
78
|
# Check migration status and wait briefly if needed
|
|
76
79
|
from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
|
|
77
80
|
|
|
78
|
-
migration_status = await wait_for_migration_or_return_status(
|
|
81
|
+
migration_status = await wait_for_migration_or_return_status(
|
|
82
|
+
timeout=5.0, project_name=active_project.name
|
|
83
|
+
)
|
|
79
84
|
if migration_status: # pragma: no cover
|
|
80
85
|
return f"# System Status\n\n{migration_status}\n\nPlease wait for migration to complete before creating notes."
|
|
81
86
|
|
|
@@ -91,7 +96,6 @@ async def write_note(
|
|
|
91
96
|
content=content,
|
|
92
97
|
entity_metadata=metadata,
|
|
93
98
|
)
|
|
94
|
-
active_project = get_active_project(project)
|
|
95
99
|
project_url = active_project.project_url
|
|
96
100
|
|
|
97
101
|
# Create or update via knowledge API
|
|
@@ -102,14 +102,14 @@ class EntityRepository(Repository[Entity]):
|
|
|
102
102
|
|
|
103
103
|
async def upsert_entity(self, entity: Entity) -> Entity:
|
|
104
104
|
"""Insert or update entity using a hybrid approach.
|
|
105
|
-
|
|
105
|
+
|
|
106
106
|
This method provides a cleaner alternative to the try/catch approach
|
|
107
|
-
for handling permalink and file_path conflicts. It first tries direct
|
|
107
|
+
for handling permalink and file_path conflicts. It first tries direct
|
|
108
108
|
insertion, then handles conflicts intelligently.
|
|
109
|
-
|
|
109
|
+
|
|
110
110
|
Args:
|
|
111
111
|
entity: The entity to insert or update
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
Returns:
|
|
114
114
|
The inserted or updated entity
|
|
115
115
|
"""
|
|
@@ -117,98 +117,102 @@ class EntityRepository(Repository[Entity]):
|
|
|
117
117
|
async with db.scoped_session(self.session_maker) as session:
|
|
118
118
|
# Set project_id if applicable and not already set
|
|
119
119
|
self._set_project_id_if_needed(entity)
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
# Check for existing entity with same file_path first
|
|
122
122
|
existing_by_path = await session.execute(
|
|
123
123
|
select(Entity).where(
|
|
124
|
-
Entity.file_path == entity.file_path,
|
|
125
|
-
Entity.project_id == entity.project_id
|
|
124
|
+
Entity.file_path == entity.file_path, Entity.project_id == entity.project_id
|
|
126
125
|
)
|
|
127
126
|
)
|
|
128
127
|
existing_path_entity = existing_by_path.scalar_one_or_none()
|
|
129
|
-
|
|
128
|
+
|
|
130
129
|
if existing_path_entity:
|
|
131
130
|
# Update existing entity with same file path
|
|
132
131
|
for key, value in {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
132
|
+
"title": entity.title,
|
|
133
|
+
"entity_type": entity.entity_type,
|
|
134
|
+
"entity_metadata": entity.entity_metadata,
|
|
135
|
+
"content_type": entity.content_type,
|
|
136
|
+
"permalink": entity.permalink,
|
|
137
|
+
"checksum": entity.checksum,
|
|
138
|
+
"updated_at": entity.updated_at,
|
|
140
139
|
}.items():
|
|
141
140
|
setattr(existing_path_entity, key, value)
|
|
142
|
-
|
|
141
|
+
|
|
143
142
|
await session.flush()
|
|
144
143
|
# Return with relationships loaded
|
|
145
144
|
query = (
|
|
146
|
-
select(
|
|
145
|
+
self.select()
|
|
147
146
|
.where(Entity.file_path == entity.file_path)
|
|
148
147
|
.options(*self.get_load_options())
|
|
149
148
|
)
|
|
150
149
|
result = await session.execute(query)
|
|
151
150
|
found = result.scalar_one_or_none()
|
|
152
151
|
if not found: # pragma: no cover
|
|
153
|
-
raise RuntimeError(
|
|
152
|
+
raise RuntimeError(
|
|
153
|
+
f"Failed to retrieve entity after update: {entity.file_path}"
|
|
154
|
+
)
|
|
154
155
|
return found
|
|
155
|
-
|
|
156
|
+
|
|
156
157
|
# No existing entity with same file_path, try insert
|
|
157
158
|
try:
|
|
158
159
|
# Simple insert for new entity
|
|
159
160
|
session.add(entity)
|
|
160
161
|
await session.flush()
|
|
161
|
-
|
|
162
|
+
|
|
162
163
|
# Return with relationships loaded
|
|
163
164
|
query = (
|
|
164
|
-
select(
|
|
165
|
+
self.select()
|
|
165
166
|
.where(Entity.file_path == entity.file_path)
|
|
166
167
|
.options(*self.get_load_options())
|
|
167
168
|
)
|
|
168
169
|
result = await session.execute(query)
|
|
169
170
|
found = result.scalar_one_or_none()
|
|
170
171
|
if not found: # pragma: no cover
|
|
171
|
-
raise RuntimeError(
|
|
172
|
+
raise RuntimeError(
|
|
173
|
+
f"Failed to retrieve entity after insert: {entity.file_path}"
|
|
174
|
+
)
|
|
172
175
|
return found
|
|
173
|
-
|
|
176
|
+
|
|
174
177
|
except IntegrityError:
|
|
175
178
|
# Could be either file_path or permalink conflict
|
|
176
179
|
await session.rollback()
|
|
177
|
-
|
|
180
|
+
|
|
178
181
|
# Check if it's a file_path conflict (race condition)
|
|
179
182
|
existing_by_path_check = await session.execute(
|
|
180
183
|
select(Entity).where(
|
|
181
|
-
Entity.file_path == entity.file_path,
|
|
182
|
-
Entity.project_id == entity.project_id
|
|
184
|
+
Entity.file_path == entity.file_path, Entity.project_id == entity.project_id
|
|
183
185
|
)
|
|
184
186
|
)
|
|
185
187
|
race_condition_entity = existing_by_path_check.scalar_one_or_none()
|
|
186
|
-
|
|
188
|
+
|
|
187
189
|
if race_condition_entity:
|
|
188
190
|
# Race condition: file_path conflict detected after our initial check
|
|
189
191
|
# Update the existing entity instead
|
|
190
192
|
for key, value in {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
193
|
+
"title": entity.title,
|
|
194
|
+
"entity_type": entity.entity_type,
|
|
195
|
+
"entity_metadata": entity.entity_metadata,
|
|
196
|
+
"content_type": entity.content_type,
|
|
197
|
+
"permalink": entity.permalink,
|
|
198
|
+
"checksum": entity.checksum,
|
|
199
|
+
"updated_at": entity.updated_at,
|
|
198
200
|
}.items():
|
|
199
201
|
setattr(race_condition_entity, key, value)
|
|
200
|
-
|
|
202
|
+
|
|
201
203
|
await session.flush()
|
|
202
204
|
# Return the updated entity with relationships loaded
|
|
203
205
|
query = (
|
|
204
|
-
select(
|
|
206
|
+
self.select()
|
|
205
207
|
.where(Entity.file_path == entity.file_path)
|
|
206
208
|
.options(*self.get_load_options())
|
|
207
209
|
)
|
|
208
210
|
result = await session.execute(query)
|
|
209
211
|
found = result.scalar_one_or_none()
|
|
210
212
|
if not found: # pragma: no cover
|
|
211
|
-
raise RuntimeError(
|
|
213
|
+
raise RuntimeError(
|
|
214
|
+
f"Failed to retrieve entity after race condition update: {entity.file_path}"
|
|
215
|
+
)
|
|
212
216
|
return found
|
|
213
217
|
else:
|
|
214
218
|
# Must be permalink conflict - generate unique permalink
|
|
@@ -218,14 +222,13 @@ class EntityRepository(Repository[Entity]):
|
|
|
218
222
|
"""Handle permalink conflicts by generating a unique permalink."""
|
|
219
223
|
base_permalink = entity.permalink
|
|
220
224
|
suffix = 1
|
|
221
|
-
|
|
225
|
+
|
|
222
226
|
# Find a unique permalink
|
|
223
227
|
while True:
|
|
224
228
|
test_permalink = f"{base_permalink}-{suffix}"
|
|
225
229
|
existing = await session.execute(
|
|
226
230
|
select(Entity).where(
|
|
227
|
-
Entity.permalink == test_permalink,
|
|
228
|
-
Entity.project_id == entity.project_id
|
|
231
|
+
Entity.permalink == test_permalink, Entity.project_id == entity.project_id
|
|
229
232
|
)
|
|
230
233
|
)
|
|
231
234
|
if existing.scalar_one_or_none() is None:
|
|
@@ -233,14 +236,14 @@ class EntityRepository(Repository[Entity]):
|
|
|
233
236
|
entity.permalink = test_permalink
|
|
234
237
|
break
|
|
235
238
|
suffix += 1
|
|
236
|
-
|
|
239
|
+
|
|
237
240
|
# Insert with unique permalink (no conflict possible now)
|
|
238
241
|
session.add(entity)
|
|
239
242
|
await session.flush()
|
|
240
|
-
|
|
243
|
+
|
|
241
244
|
# Return the inserted entity with relationships loaded
|
|
242
245
|
query = (
|
|
243
|
-
select(
|
|
246
|
+
self.select()
|
|
244
247
|
.where(Entity.file_path == entity.file_path)
|
|
245
248
|
.options(*self.get_load_options())
|
|
246
249
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Repository for search operations."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import re
|
|
4
5
|
import time
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from datetime import datetime
|
|
@@ -120,23 +121,141 @@ class SearchRepository:
|
|
|
120
121
|
logger.error(f"Error initializing search index: {e}")
|
|
121
122
|
raise e
|
|
122
123
|
|
|
123
|
-
def
|
|
124
|
-
"""Prepare a
|
|
124
|
+
def _prepare_boolean_query(self, query: str) -> str:
|
|
125
|
+
"""Prepare a Boolean query by quoting individual terms while preserving operators.
|
|
125
126
|
|
|
126
127
|
Args:
|
|
127
|
-
|
|
128
|
+
query: A Boolean query like "tier1-test AND unicode" or "(hello OR world) NOT test"
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
A properly formatted Boolean query with quoted terms that need quoting
|
|
132
|
+
"""
|
|
133
|
+
# Define Boolean operators and their boundaries
|
|
134
|
+
boolean_pattern = r"(\bAND\b|\bOR\b|\bNOT\b)"
|
|
135
|
+
|
|
136
|
+
# Split the query by Boolean operators, keeping the operators
|
|
137
|
+
parts = re.split(boolean_pattern, query)
|
|
138
|
+
|
|
139
|
+
processed_parts = []
|
|
140
|
+
for part in parts:
|
|
141
|
+
part = part.strip()
|
|
142
|
+
if not part:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# If it's a Boolean operator, keep it as is
|
|
146
|
+
if part in ["AND", "OR", "NOT"]:
|
|
147
|
+
processed_parts.append(part)
|
|
148
|
+
else:
|
|
149
|
+
# Handle parentheses specially - they should be preserved for grouping
|
|
150
|
+
if "(" in part or ")" in part:
|
|
151
|
+
# Parse parenthetical expressions carefully
|
|
152
|
+
processed_part = self._prepare_parenthetical_term(part)
|
|
153
|
+
processed_parts.append(processed_part)
|
|
154
|
+
else:
|
|
155
|
+
# This is a search term - for Boolean queries, don't add prefix wildcards
|
|
156
|
+
prepared_term = self._prepare_single_term(part, is_prefix=False)
|
|
157
|
+
processed_parts.append(prepared_term)
|
|
158
|
+
|
|
159
|
+
return " ".join(processed_parts)
|
|
160
|
+
|
|
161
|
+
def _prepare_parenthetical_term(self, term: str) -> str:
|
|
162
|
+
"""Prepare a term that contains parentheses, preserving the parentheses for grouping.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
term: A term that may contain parentheses like "(hello" or "world)" or "(hello OR world)"
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
A properly formatted term with parentheses preserved
|
|
169
|
+
"""
|
|
170
|
+
# Handle terms that start/end with parentheses but may contain quotable content
|
|
171
|
+
result = ""
|
|
172
|
+
i = 0
|
|
173
|
+
while i < len(term):
|
|
174
|
+
if term[i] in "()":
|
|
175
|
+
# Preserve parentheses as-is
|
|
176
|
+
result += term[i]
|
|
177
|
+
i += 1
|
|
178
|
+
else:
|
|
179
|
+
# Find the next parenthesis or end of string
|
|
180
|
+
start = i
|
|
181
|
+
while i < len(term) and term[i] not in "()":
|
|
182
|
+
i += 1
|
|
183
|
+
|
|
184
|
+
# Extract the content between parentheses
|
|
185
|
+
content = term[start:i].strip()
|
|
186
|
+
if content:
|
|
187
|
+
# Only quote if it actually needs quoting (has hyphens, special chars, etc)
|
|
188
|
+
# but don't quote if it's just simple words
|
|
189
|
+
if self._needs_quoting(content):
|
|
190
|
+
escaped_content = content.replace('"', '""')
|
|
191
|
+
result += f'"{escaped_content}"'
|
|
192
|
+
else:
|
|
193
|
+
result += content
|
|
194
|
+
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
def _needs_quoting(self, term: str) -> bool:
|
|
198
|
+
"""Check if a term needs to be quoted for FTS5 safety.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
term: The term to check
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
True if the term should be quoted
|
|
205
|
+
"""
|
|
206
|
+
if not term or not term.strip():
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
# Characters that indicate we should quote (excluding parentheses which are valid syntax)
|
|
210
|
+
needs_quoting_chars = [
|
|
211
|
+
" ",
|
|
212
|
+
".",
|
|
213
|
+
":",
|
|
214
|
+
";",
|
|
215
|
+
",",
|
|
216
|
+
"<",
|
|
217
|
+
">",
|
|
218
|
+
"?",
|
|
219
|
+
"/",
|
|
220
|
+
"-",
|
|
221
|
+
"'",
|
|
222
|
+
'"',
|
|
223
|
+
"[",
|
|
224
|
+
"]",
|
|
225
|
+
"{",
|
|
226
|
+
"}",
|
|
227
|
+
"+",
|
|
228
|
+
"!",
|
|
229
|
+
"@",
|
|
230
|
+
"#",
|
|
231
|
+
"$",
|
|
232
|
+
"%",
|
|
233
|
+
"^",
|
|
234
|
+
"&",
|
|
235
|
+
"=",
|
|
236
|
+
"|",
|
|
237
|
+
"\\",
|
|
238
|
+
"~",
|
|
239
|
+
"`",
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
return any(c in term for c in needs_quoting_chars)
|
|
243
|
+
|
|
244
|
+
def _prepare_single_term(self, term: str, is_prefix: bool = True) -> str:
|
|
245
|
+
"""Prepare a single search term (no Boolean operators).
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
term: A single search term
|
|
128
249
|
is_prefix: Whether to add prefix search capability (* suffix)
|
|
129
250
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
- Terms with FTS5 special characters are quoted to prevent syntax errors
|
|
133
|
-
- Simple terms get prefix wildcards for better matching
|
|
251
|
+
Returns:
|
|
252
|
+
A properly formatted single term
|
|
134
253
|
"""
|
|
135
|
-
|
|
136
|
-
boolean_operators = [" AND ", " OR ", " NOT "]
|
|
137
|
-
if any(op in f" {term} " for op in boolean_operators):
|
|
254
|
+
if not term or not term.strip():
|
|
138
255
|
return term
|
|
139
256
|
|
|
257
|
+
term = term.strip()
|
|
258
|
+
|
|
140
259
|
# Check if term is already a proper wildcard pattern (alphanumeric + *)
|
|
141
260
|
# e.g., "hello*", "test*world" - these should be left alone
|
|
142
261
|
if "*" in term and all(c.isalnum() or c in "*_-" for c in term):
|
|
@@ -218,6 +337,26 @@ class SearchRepository:
|
|
|
218
337
|
|
|
219
338
|
return term
|
|
220
339
|
|
|
340
|
+
def _prepare_search_term(self, term: str, is_prefix: bool = True) -> str:
|
|
341
|
+
"""Prepare a search term for FTS5 query.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
term: The search term to prepare
|
|
345
|
+
is_prefix: Whether to add prefix search capability (* suffix)
|
|
346
|
+
|
|
347
|
+
For FTS5:
|
|
348
|
+
- Boolean operators (AND, OR, NOT) are preserved for complex queries
|
|
349
|
+
- Terms with FTS5 special characters are quoted to prevent syntax errors
|
|
350
|
+
- Simple terms get prefix wildcards for better matching
|
|
351
|
+
"""
|
|
352
|
+
# Check for explicit boolean operators - if present, process as Boolean query
|
|
353
|
+
boolean_operators = [" AND ", " OR ", " NOT "]
|
|
354
|
+
if any(op in f" {term} " for op in boolean_operators):
|
|
355
|
+
return self._prepare_boolean_query(term)
|
|
356
|
+
|
|
357
|
+
# For non-Boolean queries, use the single term preparation logic
|
|
358
|
+
return self._prepare_single_term(term, is_prefix)
|
|
359
|
+
|
|
221
360
|
async def search(
|
|
222
361
|
self,
|
|
223
362
|
search_text: Optional[str] = None,
|
|
@@ -242,19 +381,10 @@ class SearchRepository:
|
|
|
242
381
|
# For wildcard searches, don't add any text conditions - return all results
|
|
243
382
|
pass
|
|
244
383
|
else:
|
|
245
|
-
#
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
# If boolean operators are present, use the raw query
|
|
250
|
-
# No need to prepare it, FTS5 will understand the operators
|
|
251
|
-
params["text"] = search_text
|
|
252
|
-
conditions.append("(title MATCH :text OR content_stems MATCH :text)")
|
|
253
|
-
else:
|
|
254
|
-
# Standard search with term preparation
|
|
255
|
-
processed_text = self._prepare_search_term(search_text.strip())
|
|
256
|
-
params["text"] = processed_text
|
|
257
|
-
conditions.append("(title MATCH :text OR content_stems MATCH :text)")
|
|
384
|
+
# Use _prepare_search_term to handle both Boolean and non-Boolean queries
|
|
385
|
+
processed_text = self._prepare_search_term(search_text.strip())
|
|
386
|
+
params["text"] = processed_text
|
|
387
|
+
conditions.append("(title MATCH :text OR content_stems MATCH :text)")
|
|
258
388
|
|
|
259
389
|
# Handle title match search
|
|
260
390
|
if title:
|
basic_memory/schemas/memory.py
CHANGED
|
@@ -302,7 +302,7 @@ class EntityService(BaseService[EntityModel]):
|
|
|
302
302
|
|
|
303
303
|
Creates the entity with null checksum to indicate sync not complete.
|
|
304
304
|
Relations will be added in second pass.
|
|
305
|
-
|
|
305
|
+
|
|
306
306
|
Uses UPSERT approach to handle permalink/file_path conflicts cleanly.
|
|
307
307
|
"""
|
|
308
308
|
logger.debug(f"Creating entity: {markdown.frontmatter.title} file_path: {file_path}")
|
|
@@ -310,7 +310,7 @@ class EntityService(BaseService[EntityModel]):
|
|
|
310
310
|
|
|
311
311
|
# Mark as incomplete because we still need to add relations
|
|
312
312
|
model.checksum = None
|
|
313
|
-
|
|
313
|
+
|
|
314
314
|
# Use UPSERT to handle conflicts cleanly
|
|
315
315
|
try:
|
|
316
316
|
return await self.repository.upsert_entity(model)
|
|
@@ -682,8 +682,8 @@ class EntityService(BaseService[EntityModel]):
|
|
|
682
682
|
# 6. Prepare database updates
|
|
683
683
|
updates = {"file_path": destination_path}
|
|
684
684
|
|
|
685
|
-
# 7. Update permalink if configured
|
|
686
|
-
if app_config.update_permalinks_on_move:
|
|
685
|
+
# 7. Update permalink if configured or if entity has null permalink
|
|
686
|
+
if app_config.update_permalinks_on_move or old_permalink is None:
|
|
687
687
|
# Generate new permalink from destination path
|
|
688
688
|
new_permalink = await self.resolve_permalink(destination_path)
|
|
689
689
|
|
|
@@ -693,7 +693,12 @@ class EntityService(BaseService[EntityModel]):
|
|
|
693
693
|
)
|
|
694
694
|
|
|
695
695
|
updates["permalink"] = new_permalink
|
|
696
|
-
|
|
696
|
+
if old_permalink is None:
|
|
697
|
+
logger.info(
|
|
698
|
+
f"Generated permalink for entity with null permalink: {new_permalink}"
|
|
699
|
+
)
|
|
700
|
+
else:
|
|
701
|
+
logger.info(f"Updated permalink: {old_permalink} -> {new_permalink}")
|
|
697
702
|
|
|
698
703
|
# 8. Recalculate checksum
|
|
699
704
|
new_checksum = await self.file_service.compute_checksum(destination_path)
|
|
@@ -21,9 +21,9 @@ async def initialize_database(app_config: BasicMemoryConfig) -> None:
|
|
|
21
21
|
|
|
22
22
|
Args:
|
|
23
23
|
app_config: The Basic Memory project configuration
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
Note:
|
|
26
|
-
Database migrations are now handled automatically when the database
|
|
26
|
+
Database migrations are now handled automatically when the database
|
|
27
27
|
connection is first established via get_or_create_db().
|
|
28
28
|
"""
|
|
29
29
|
# Trigger database initialization and migrations by getting the database connection
|
|
@@ -50,7 +50,9 @@ async def reconcile_projects_with_config(app_config: BasicMemoryConfig):
|
|
|
50
50
|
|
|
51
51
|
# Get database session - migrations handled centrally
|
|
52
52
|
_, session_maker = await db.get_or_create_db(
|
|
53
|
-
db_path=app_config.database_path,
|
|
53
|
+
db_path=app_config.database_path,
|
|
54
|
+
db_type=db.DatabaseType.FILESYSTEM,
|
|
55
|
+
ensure_migrations=False,
|
|
54
56
|
)
|
|
55
57
|
project_repository = ProjectRepository(session_maker)
|
|
56
58
|
|
|
@@ -71,7 +73,9 @@ async def reconcile_projects_with_config(app_config: BasicMemoryConfig):
|
|
|
71
73
|
async def migrate_legacy_projects(app_config: BasicMemoryConfig):
|
|
72
74
|
# Get database session - migrations handled centrally
|
|
73
75
|
_, session_maker = await db.get_or_create_db(
|
|
74
|
-
db_path=app_config.database_path,
|
|
76
|
+
db_path=app_config.database_path,
|
|
77
|
+
db_type=db.DatabaseType.FILESYSTEM,
|
|
78
|
+
ensure_migrations=False,
|
|
75
79
|
)
|
|
76
80
|
logger.info("Migrating legacy projects...")
|
|
77
81
|
project_repository = ProjectRepository(session_maker)
|
|
@@ -140,7 +144,9 @@ async def initialize_file_sync(
|
|
|
140
144
|
|
|
141
145
|
# Load app configuration - migrations handled centrally
|
|
142
146
|
_, session_maker = await db.get_or_create_db(
|
|
143
|
-
db_path=app_config.database_path,
|
|
147
|
+
db_path=app_config.database_path,
|
|
148
|
+
db_type=db.DatabaseType.FILESYSTEM,
|
|
149
|
+
ensure_migrations=False,
|
|
144
150
|
)
|
|
145
151
|
project_repository = ProjectRepository(session_maker)
|
|
146
152
|
|
|
@@ -154,6 +154,15 @@ class ProjectService:
|
|
|
154
154
|
|
|
155
155
|
logger.info(f"Project '{name}' set as default in configuration and database")
|
|
156
156
|
|
|
157
|
+
# Refresh MCP session to pick up the new default project
|
|
158
|
+
try:
|
|
159
|
+
from basic_memory.mcp.project_session import session
|
|
160
|
+
|
|
161
|
+
session.refresh_from_config()
|
|
162
|
+
except ImportError: # pragma: no cover
|
|
163
|
+
# MCP components might not be available in all contexts (e.g., CLI-only usage)
|
|
164
|
+
logger.debug("MCP session not available, skipping session refresh")
|
|
165
|
+
|
|
157
166
|
async def _ensure_single_default_project(self) -> None:
|
|
158
167
|
"""Ensure only one project has is_default=True.
|
|
159
168
|
|
|
@@ -274,6 +283,15 @@ class ProjectService:
|
|
|
274
283
|
|
|
275
284
|
logger.info("Project synchronization complete")
|
|
276
285
|
|
|
286
|
+
# Refresh MCP session to ensure it's in sync with current config
|
|
287
|
+
try:
|
|
288
|
+
from basic_memory.mcp.project_session import session
|
|
289
|
+
|
|
290
|
+
session.refresh_from_config()
|
|
291
|
+
except ImportError:
|
|
292
|
+
# MCP components might not be available in all contexts
|
|
293
|
+
logger.debug("MCP session not available, skipping session refresh")
|
|
294
|
+
|
|
277
295
|
async def update_project( # pragma: no cover
|
|
278
296
|
self, name: str, updated_path: Optional[str] = None, is_active: Optional[bool] = None
|
|
279
297
|
) -> None:
|
|
@@ -131,6 +131,23 @@ class SyncStatusTracker:
|
|
|
131
131
|
"""Check if system is ready (no sync in progress)."""
|
|
132
132
|
return self._global_status in (SyncStatus.IDLE, SyncStatus.COMPLETED)
|
|
133
133
|
|
|
134
|
+
def is_project_ready(self, project_name: str) -> bool:
|
|
135
|
+
"""Check if a specific project is ready for operations.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
project_name: Name of the project to check
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if the project is ready (completed, watching, or not tracked),
|
|
142
|
+
False if the project is syncing, scanning, or failed
|
|
143
|
+
"""
|
|
144
|
+
project_status = self._project_statuses.get(project_name)
|
|
145
|
+
if not project_status:
|
|
146
|
+
# Project not tracked = ready (likely hasn't been synced yet)
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
return project_status.status in (SyncStatus.COMPLETED, SyncStatus.WATCHING, SyncStatus.IDLE)
|
|
150
|
+
|
|
134
151
|
def get_project_status(self, project_name: str) -> Optional[ProjectSyncStatus]:
|
|
135
152
|
"""Get status for a specific project."""
|
|
136
153
|
return self._project_statuses.get(project_name)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: basic-memory
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0b1
|
|
4
4
|
Summary: Local-first knowledge management combining Zettelkasten with knowledge graphs
|
|
5
5
|
Project-URL: Homepage, https://github.com/basicmachines-co/basic-memory
|
|
6
6
|
Project-URL: Repository, https://github.com/basicmachines-co/basic-memory
|
|
@@ -449,6 +449,31 @@ Development versions are automatically published on every commit to main with ve
|
|
|
449
449
|
pip install basic-memory --pre --force-reinstall
|
|
450
450
|
```
|
|
451
451
|
|
|
452
|
+
### Docker
|
|
453
|
+
|
|
454
|
+
Run Basic Memory in a container with volume mounting for your Obsidian vault:
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
# Clone and start with Docker Compose
|
|
458
|
+
git clone https://github.com/basicmachines-co/basic-memory.git
|
|
459
|
+
cd basic-memory
|
|
460
|
+
|
|
461
|
+
# Edit docker-compose.yml to point to your Obsidian vault
|
|
462
|
+
# Then start the container
|
|
463
|
+
docker-compose up -d
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Or use Docker directly:
|
|
467
|
+
```bash
|
|
468
|
+
docker run -d \
|
|
469
|
+
--name basic-memory-server \
|
|
470
|
+
-v /path/to/your/obsidian-vault:/data/knowledge:rw \
|
|
471
|
+
-v basic-memory-config:/root/.basic-memory:rw \
|
|
472
|
+
ghcr.io/basicmachines-co/basic-memory:latest
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
See [Docker Setup Guide](docs/Docker.md) for detailed configuration options, multiple project setup, and integration examples.
|
|
476
|
+
|
|
452
477
|
## License
|
|
453
478
|
|
|
454
479
|
AGPL-3.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
basic_memory/__init__.py,sha256=
|
|
2
|
-
basic_memory/config.py,sha256=
|
|
3
|
-
basic_memory/db.py,sha256=
|
|
1
|
+
basic_memory/__init__.py,sha256=DPlOFFx4PSC_e78q8BXXJTJMjPtdv3jAI7UszpA20Dk,258
|
|
2
|
+
basic_memory/config.py,sha256=YX6pP8aOMlIx9NoCeKLS0b5cgOnegbWhX2ijJzimLQg,11828
|
|
3
|
+
basic_memory/db.py,sha256=bFuJHj_PGEhaj5ZgRItIUSW0ujAFCGgYKO7nZsjbYD0,7582
|
|
4
4
|
basic_memory/deps.py,sha256=zXOhqXCoSVIa1iIcO8U6uUiofJn5eT4ycwJkH9I2kX4,12102
|
|
5
5
|
basic_memory/file_utils.py,sha256=eaxTKLLEbTIy_Mb_Iv_Dmt4IXAJSrZGVi-Knrpyci3E,6700
|
|
6
6
|
basic_memory/utils.py,sha256=BL6DDRiMF1gNcDr_guRAYflooSrSlDniJh96ApdzuDY,7555
|
|
@@ -27,19 +27,19 @@ basic_memory/api/routers/project_router.py,sha256=cXGx6VZMg67jdzMi1Xf8SodtueEI04
|
|
|
27
27
|
basic_memory/api/routers/prompt_router.py,sha256=4wxq6-NREgVJM8N9C0YsN1AAUDD8nkTCOzWyzSqTSFw,9948
|
|
28
28
|
basic_memory/api/routers/resource_router.py,sha256=WEJEqEaY_yTKj5-U-rW4kXQKUcJflykgwI6_g_R41ck,8058
|
|
29
29
|
basic_memory/api/routers/search_router.py,sha256=GD62jlCQTiL_VNsdibi-b1f6H40KCWo9SX2Cl7YH4QU,1226
|
|
30
|
-
basic_memory/api/routers/utils.py,sha256=
|
|
30
|
+
basic_memory/api/routers/utils.py,sha256=nmD1faJOHcnWQjbCanojUwA9xhinf764U8SUqjNXpXw,5159
|
|
31
31
|
basic_memory/cli/__init__.py,sha256=arcKLAWRDhPD7x5t80MlviZeYzwHZ0GZigyy3NKVoGk,33
|
|
32
32
|
basic_memory/cli/app.py,sha256=BCIaiJBPV0ipk8KwPRLNiG2ACKBQH9wo1ewKZm7CV7o,2269
|
|
33
33
|
basic_memory/cli/main.py,sha256=_0eW-TWYxj8WyCHSS9kFcrrntpeIsqJrA2P0n6LfFvY,475
|
|
34
34
|
basic_memory/cli/commands/__init__.py,sha256=jtnGMOIgOBNJCXQ8xpKbLWhFUD3qgpOraRi19YRNaTc,411
|
|
35
35
|
basic_memory/cli/commands/auth.py,sha256=mnTlb75eeQWocWYs67LaYbuEysaYvHkqP7gQ0FEXKjE,4858
|
|
36
|
-
basic_memory/cli/commands/db.py,sha256=
|
|
36
|
+
basic_memory/cli/commands/db.py,sha256=vhuXruyNBj9-9cQzYqKL29aEDUJE7IUQCxodu0wHuio,1481
|
|
37
37
|
basic_memory/cli/commands/import_chatgpt.py,sha256=VU_Kfr4B0lVh_Z2_bmsTO3e3U40wrAOvTAvtblgjses,2773
|
|
38
38
|
basic_memory/cli/commands/import_claude_conversations.py,sha256=sXnP0hjfwUapwHQDzxE2HEkCDe8FTaE4cogKILsD1EA,2866
|
|
39
39
|
basic_memory/cli/commands/import_claude_projects.py,sha256=mWYIeA-mu_Pq23R7OEtY2XHXG5CAh1dMGIBhckB4zRk,2811
|
|
40
40
|
basic_memory/cli/commands/import_memory_json.py,sha256=Vz5rt7KCel5B3Dtv57WPEUJTHCMwFUqQlOCm2djwUi8,2867
|
|
41
|
-
basic_memory/cli/commands/mcp.py,sha256=
|
|
42
|
-
basic_memory/cli/commands/project.py,sha256=
|
|
41
|
+
basic_memory/cli/commands/mcp.py,sha256=NCqyY5dFHDtYnMDrWQiquoGbALUh-AqN4iRjJlFngxY,3123
|
|
42
|
+
basic_memory/cli/commands/project.py,sha256=9OJWoV9kSkLePETUPxNs0v5YceXLgPWO2fU45ZinP9g,12252
|
|
43
43
|
basic_memory/cli/commands/status.py,sha256=708EK8-iPjyc1iE5MPECzAyZraGYoGpvYjLwTm-BlQs,5719
|
|
44
44
|
basic_memory/cli/commands/sync.py,sha256=gOU_onrMj9_IRiIe8FWU_FLEvfjcOt-qhrvvFJuU-ws,8010
|
|
45
45
|
basic_memory/cli/commands/tool.py,sha256=my-kALn3khv1W2Avi736NrHsfkpbyP57mDi5LjHwqe0,9540
|
|
@@ -60,53 +60,53 @@ basic_memory/mcp/__init__.py,sha256=dsDOhKqjYeIbCULbHIxfcItTbqudEuEg1Np86eq0GEQ,
|
|
|
60
60
|
basic_memory/mcp/async_client.py,sha256=Eo345wANiBRSM4u3j_Vd6Ax4YtMg7qbWd9PIoFfj61I,236
|
|
61
61
|
basic_memory/mcp/auth_provider.py,sha256=CTydkEBvxh_fg_Z0hxKjTT8nHJoFhxrwp5hTQuToiIU,9977
|
|
62
62
|
basic_memory/mcp/external_auth_provider.py,sha256=Z1GDbr6P4C-flZVHMWkIqAu30kcfeHv2iSp0EYbFuxo,11483
|
|
63
|
-
basic_memory/mcp/project_session.py,sha256=
|
|
64
|
-
basic_memory/mcp/server.py,sha256=
|
|
63
|
+
basic_memory/mcp/project_session.py,sha256=KfObBqUFUKNGlcApCfQcsqMYsmtWs72OdIcQ79ZSWhk,4142
|
|
64
|
+
basic_memory/mcp/server.py,sha256=T8utX0fTA12rAC_TjtWgsfB1z-Q6pdTWJH4HISw73vg,3764
|
|
65
65
|
basic_memory/mcp/supabase_auth_provider.py,sha256=MLHfSHjdx2Q5jr_Ljx0qZBaOwp7CkPdk_ybR_LQ7Mvw,16472
|
|
66
66
|
basic_memory/mcp/prompts/__init__.py,sha256=UvaIw5KA8PaXj3Wz1Dr-VjlkEq6T5D8AGtYFVwaHqnA,683
|
|
67
67
|
basic_memory/mcp/prompts/ai_assistant_guide.py,sha256=8TI5xObiRVcwv6w9by1xQHlX0whvyE7-LGsiqDMRTFg,821
|
|
68
68
|
basic_memory/mcp/prompts/continue_conversation.py,sha256=rsmlC2V7e7G6DAK0K825vFsPKgsRQ702HFzn6lkHaDM,1998
|
|
69
69
|
basic_memory/mcp/prompts/recent_activity.py,sha256=0v1c3b2SdDDxXVuF8eOjNooYy04uRYel0pdJ0rnggw4,3311
|
|
70
70
|
basic_memory/mcp/prompts/search.py,sha256=nb88MZy9tdW_MmCLUVItiukrLdb3xEHWLv0JVLUlc4o,1692
|
|
71
|
-
basic_memory/mcp/prompts/sync_status.py,sha256=
|
|
71
|
+
basic_memory/mcp/prompts/sync_status.py,sha256=0F6YowgqIbAFmGE3vFFJ-D-q1SrTqzGLKYWECgNWaxw,4495
|
|
72
72
|
basic_memory/mcp/prompts/utils.py,sha256=VacrbqwYtySpIlYIrKHo5s6jtoTMscYJqrFRH3zpC6Q,5431
|
|
73
73
|
basic_memory/mcp/resources/ai_assistant_guide.md,sha256=qnYWDkYlb-JmKuOoZ5llmRas_t4dWDXB_i8LE277Lgs,14777
|
|
74
74
|
basic_memory/mcp/resources/project_info.py,sha256=LcUkTx4iXBfU6Lp4TVch78OqLopbOy4ljyKnfr4VXso,1906
|
|
75
75
|
basic_memory/mcp/tools/__init__.py,sha256=lCCOC0jElvL2v53WI_dxRs4qABq4Eo-YGm6j2XeZ6AQ,1591
|
|
76
|
-
basic_memory/mcp/tools/build_context.py,sha256=
|
|
76
|
+
basic_memory/mcp/tools/build_context.py,sha256=ckKAt3uPXz5hzT_e68PuZuK8_tquo2OOai4uM_yxl44,4611
|
|
77
77
|
basic_memory/mcp/tools/canvas.py,sha256=22F9G9gfPb-l8i1B5ra4Ja_h9zYY83rPY9mDA5C5gkY,3738
|
|
78
78
|
basic_memory/mcp/tools/delete_note.py,sha256=tSyRc_VgBmLyVeenClwX1Sk--LKcGahAMzTX2mK2XIs,7346
|
|
79
79
|
basic_memory/mcp/tools/edit_note.py,sha256=q4x-f7-j_l-wzm17-AVFT1_WGCo0Cq4lI3seYSe21aY,13570
|
|
80
80
|
basic_memory/mcp/tools/list_directory.py,sha256=-FxDsCru5YD02M4qkQDAurEJWyRaC7YI4YR6zg0atR8,5236
|
|
81
|
-
basic_memory/mcp/tools/move_note.py,sha256=
|
|
81
|
+
basic_memory/mcp/tools/move_note.py,sha256=jAsCFXrcWXPoBWlWcW8y3Tli5MkKwCQK-n6IwUZoOK8,17357
|
|
82
82
|
basic_memory/mcp/tools/project_management.py,sha256=sZQbak0jIQ6k03Syz6X6Zsy-C9z8KdPnEQcpJLCxPwM,12779
|
|
83
83
|
basic_memory/mcp/tools/read_content.py,sha256=4FTw13B8UjVVhR78NJB9HKeJb_nA6-BGT1WdGtekN5Q,8596
|
|
84
|
-
basic_memory/mcp/tools/read_note.py,sha256=
|
|
84
|
+
basic_memory/mcp/tools/read_note.py,sha256=V08NdBqWY8Y0Q4zuwK--zN3VK7fmuCH1mOYZKwL1IT4,7614
|
|
85
85
|
basic_memory/mcp/tools/recent_activity.py,sha256=XVjNJAJnmxvzx9_Ls1A-QOd2yTR7pJlSTTuRxSivmN4,4833
|
|
86
86
|
basic_memory/mcp/tools/search.py,sha256=22sLHed6z53mH9NQqBv37Xi4d6AtOTyrUvKs2Mycijk,11296
|
|
87
87
|
basic_memory/mcp/tools/sync_status.py,sha256=mt0DdcaAlyiKW4NK4gy6psajSqcez0bOm_4MzG1NOdg,10486
|
|
88
|
-
basic_memory/mcp/tools/utils.py,sha256=
|
|
88
|
+
basic_memory/mcp/tools/utils.py,sha256=qVAEkR4naCLrqIo_7xXFubqGGxypouz-DB4_svTvARY,20892
|
|
89
89
|
basic_memory/mcp/tools/view_note.py,sha256=ddNXxyETsdA5SYflIaQVj_Cbd7I7CLVs3atRRDMbGmg,2499
|
|
90
|
-
basic_memory/mcp/tools/write_note.py,sha256=
|
|
90
|
+
basic_memory/mcp/tools/write_note.py,sha256=GFmX_VLJvcqK29-ADTCDnPgBaweAq_9IBGCs99mwFTw,6178
|
|
91
91
|
basic_memory/models/__init__.py,sha256=j0C4dtFi-FOEaQKR8dQWEG-dJtdQ15NBTiJg4nbIXNU,333
|
|
92
92
|
basic_memory/models/base.py,sha256=4hAXJ8CE1RnjKhb23lPd-QM7G_FXIdTowMJ9bRixspU,225
|
|
93
93
|
basic_memory/models/knowledge.py,sha256=AFxfKS8fRa43Kq3EjJCAufpte4VNC7fs9YfshDrB4o0,7087
|
|
94
94
|
basic_memory/models/project.py,sha256=oUrQaUOu7_muSl-i38Dh0HzmCFrMAtwgxALDUTt9k5c,2773
|
|
95
95
|
basic_memory/models/search.py,sha256=PhQ8w4taApSvjh1DpPhB4cH9GTt2E2po-DFZzhnoZkY,1300
|
|
96
96
|
basic_memory/repository/__init__.py,sha256=MWK-o8QikqzOpe5SyPbKQ2ioB5BWA0Upz65tgg-E0DU,327
|
|
97
|
-
basic_memory/repository/entity_repository.py,sha256=
|
|
97
|
+
basic_memory/repository/entity_repository.py,sha256=4qjR66bI1kvGHXFo3w_owppnCFi_na6sRkoPRAJz-uA,10405
|
|
98
98
|
basic_memory/repository/observation_repository.py,sha256=qhMvHLSjaoT3Fa_cQOKsT5jYPj66GXSytEBMwLAgygQ,2943
|
|
99
99
|
basic_memory/repository/project_info_repository.py,sha256=8XLVAYKkBWQ6GbKj1iqA9OK0FGPHdTlOs7ZtfeUf9t8,338
|
|
100
100
|
basic_memory/repository/project_repository.py,sha256=sgdKxKTSiiOZTzABwUNqli7K5mbXiPiQEAc5r0RD_jQ,3159
|
|
101
101
|
basic_memory/repository/relation_repository.py,sha256=z7Oo5Zz_J-Bj6RvQDpSWR73ZLk2fxG7e7jrMbeFeJvQ,3179
|
|
102
102
|
basic_memory/repository/repository.py,sha256=MJb-cb8QZQbL-Grq_iqv4Kq75aX2yQohLIqh5T4fFxw,15224
|
|
103
|
-
basic_memory/repository/search_repository.py,sha256=
|
|
103
|
+
basic_memory/repository/search_repository.py,sha256=qXL3PRtx2sV3Do6zeTxsmsROTnkvnatSj4xObGqAvKo,21936
|
|
104
104
|
basic_memory/schemas/__init__.py,sha256=mEgIFcdTeb-v4y0gkOh_pA5zyqGbZk-9XbXqlSi6WMs,1674
|
|
105
105
|
basic_memory/schemas/base.py,sha256=Fx97DEqzOr7y9zeeseO9qVBYbOft_4OQf9EiVfhOJn4,6738
|
|
106
106
|
basic_memory/schemas/delete.py,sha256=UAR2JK99WMj3gP-yoGWlHD3eZEkvlTSRf8QoYIE-Wfw,1180
|
|
107
107
|
basic_memory/schemas/directory.py,sha256=F9_LrJqRqb_kO08GDKJzXLb2nhbYG2PdVUo5eDD_Kf4,881
|
|
108
108
|
basic_memory/schemas/importer.py,sha256=FAh-RGxuhFW2rz3HFxwLzENJOiGgbTR2hUeXZZpM3OA,663
|
|
109
|
-
basic_memory/schemas/memory.py,sha256=
|
|
109
|
+
basic_memory/schemas/memory.py,sha256=rLSpU6VT_spnLEiVeYp9lI7FH5IvdbZt19VXFuO-vtM,5833
|
|
110
110
|
basic_memory/schemas/project_info.py,sha256=fcNjUpe25_5uMmKy142ib3p5qEakzs1WJPLkgol5zyw,7047
|
|
111
111
|
basic_memory/schemas/prompt.py,sha256=SpIVfZprQT8E5uP40j3CpBc2nHKflwOo3iZD7BFPIHE,3648
|
|
112
112
|
basic_memory/schemas/request.py,sha256=Mv5EvrLZlFIiPr8dOjo_4QXvkseYhQI7cd_X2zDsxQM,3760
|
|
@@ -115,24 +115,24 @@ basic_memory/schemas/search.py,sha256=ywMsDGAQK2sO2TT5lc-da_k67OKW1x1TenXormHHWv
|
|
|
115
115
|
basic_memory/services/__init__.py,sha256=XGt8WX3fX_0K9L37Msy8HF8nlMZYIG3uQ6mUX6_iJtg,259
|
|
116
116
|
basic_memory/services/context_service.py,sha256=4ReLAF5qifA9ayOePGsVKusw1TWj8oBzRECjrsFiKPI,14462
|
|
117
117
|
basic_memory/services/directory_service.py,sha256=_YOPXseQM4knd7PIFAho9LV_E-FljVE5WVJKQ0uflZs,6017
|
|
118
|
-
basic_memory/services/entity_service.py,sha256=
|
|
118
|
+
basic_memory/services/entity_service.py,sha256=fNUWPsprigdy6DjIyGnkeBZnY81qLXRbC5qlwlpluu4,30440
|
|
119
119
|
basic_memory/services/exceptions.py,sha256=oVjQr50XQqnFq1-MNKBilI2ShtHDxypavyDk1UeyHhw,390
|
|
120
120
|
basic_memory/services/file_service.py,sha256=jCrmnEkTQ4t9HF7L_M6BL7tdDqjjzty9hpTo9AzwhvM,10059
|
|
121
|
-
basic_memory/services/initialization.py,sha256=
|
|
121
|
+
basic_memory/services/initialization.py,sha256=HN1NhFTEPHjpzBwabVkvFbJ_ldXJXuNaww4ugh7MJos,9717
|
|
122
122
|
basic_memory/services/link_resolver.py,sha256=1-_VFsvqdT5rVBHe8Jrq63U59XQ0hxGezxY8c24Tiow,4594
|
|
123
123
|
basic_memory/services/migration_service.py,sha256=pFJCSD7UgHLx1CHvtN4Df1CzDEp-CZ9Vqx4XYn1m1M0,6096
|
|
124
|
-
basic_memory/services/project_service.py,sha256=
|
|
124
|
+
basic_memory/services/project_service.py,sha256=uLIrQB6T1DY3BXrEsLdB2ZlcKnPgjubyn-g6V9vMBzA,27928
|
|
125
125
|
basic_memory/services/search_service.py,sha256=c5Ky0ufz7YPFgHhVzNRQ4OecF_JUrt7nALzpMjobW4M,12782
|
|
126
126
|
basic_memory/services/service.py,sha256=V-d_8gOV07zGIQDpL-Ksqs3ZN9l3qf3HZOK1f_YNTag,336
|
|
127
|
-
basic_memory/services/sync_status_service.py,sha256=
|
|
127
|
+
basic_memory/services/sync_status_service.py,sha256=CgJdaJ6OFvFjKHIQSVIQX8kEU389Mrz_WS6x8dx2-7c,7504
|
|
128
128
|
basic_memory/sync/__init__.py,sha256=CVHguYH457h2u2xoM8KvOilJC71XJlZ-qUh8lHcjYj4,156
|
|
129
129
|
basic_memory/sync/background_sync.py,sha256=4CEx8oP6-qD33uCeowhpzhA8wivmWxaCmSBP37h3Fs8,714
|
|
130
130
|
basic_memory/sync/sync_service.py,sha256=AxC5J1YTcPWTmA0HdzvOZBthi4-_LZ44kNF0KQoDRPw,23387
|
|
131
131
|
basic_memory/sync/watch_service.py,sha256=JAumrHUjV1lF9NtEK32jgg0myWBfLXotNXxONeIV9SM,15316
|
|
132
132
|
basic_memory/templates/prompts/continue_conversation.hbs,sha256=trrDHSXA5S0JCbInMoUJL04xvCGRB_ku1RHNQHtl6ZI,3076
|
|
133
133
|
basic_memory/templates/prompts/search.hbs,sha256=H1cCIsHKp4VC1GrH2KeUB8pGe5vXFPqb2VPotypmeCA,3098
|
|
134
|
-
basic_memory-0.
|
|
135
|
-
basic_memory-0.
|
|
136
|
-
basic_memory-0.
|
|
137
|
-
basic_memory-0.
|
|
138
|
-
basic_memory-0.
|
|
134
|
+
basic_memory-0.14.0b1.dist-info/METADATA,sha256=J0wpc3nuaEntf0KMuMgnNhQZC4L0ap2g5ST8c07QLO0,17226
|
|
135
|
+
basic_memory-0.14.0b1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
136
|
+
basic_memory-0.14.0b1.dist-info/entry_points.txt,sha256=wvE2mRF6-Pg4weIYcfQ-86NOLZD4WJg7F7TIsRVFLb8,90
|
|
137
|
+
basic_memory-0.14.0b1.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
138
|
+
basic_memory-0.14.0b1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|