basic-memory 0.16.1__py3-none-any.whl → 0.17.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of basic-memory might be problematic. Click here for more details.
- basic_memory/__init__.py +1 -1
- basic_memory/alembic/env.py +112 -26
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +15 -3
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +44 -36
- basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
- basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +13 -0
- basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
- basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
- basic_memory/api/app.py +45 -24
- basic_memory/api/container.py +133 -0
- basic_memory/api/routers/knowledge_router.py +17 -5
- basic_memory/api/routers/project_router.py +68 -14
- basic_memory/api/routers/resource_router.py +37 -27
- basic_memory/api/routers/utils.py +53 -14
- basic_memory/api/v2/__init__.py +35 -0
- basic_memory/api/v2/routers/__init__.py +21 -0
- basic_memory/api/v2/routers/directory_router.py +93 -0
- basic_memory/api/v2/routers/importer_router.py +181 -0
- basic_memory/api/v2/routers/knowledge_router.py +427 -0
- basic_memory/api/v2/routers/memory_router.py +130 -0
- basic_memory/api/v2/routers/project_router.py +359 -0
- basic_memory/api/v2/routers/prompt_router.py +269 -0
- basic_memory/api/v2/routers/resource_router.py +286 -0
- basic_memory/api/v2/routers/search_router.py +73 -0
- basic_memory/cli/app.py +43 -7
- basic_memory/cli/auth.py +27 -4
- basic_memory/cli/commands/__init__.py +3 -1
- basic_memory/cli/commands/cloud/api_client.py +20 -5
- basic_memory/cli/commands/cloud/cloud_utils.py +13 -6
- basic_memory/cli/commands/cloud/rclone_commands.py +110 -14
- basic_memory/cli/commands/cloud/rclone_installer.py +18 -4
- basic_memory/cli/commands/cloud/upload.py +10 -3
- basic_memory/cli/commands/command_utils.py +52 -4
- basic_memory/cli/commands/db.py +78 -19
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +12 -8
- basic_memory/cli/commands/import_claude_conversations.py +12 -8
- basic_memory/cli/commands/import_claude_projects.py +12 -8
- basic_memory/cli/commands/import_memory_json.py +12 -8
- basic_memory/cli/commands/mcp.py +8 -26
- basic_memory/cli/commands/project.py +22 -9
- basic_memory/cli/commands/status.py +3 -2
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/container.py +84 -0
- basic_memory/cli/main.py +7 -0
- basic_memory/config.py +177 -77
- basic_memory/db.py +183 -77
- basic_memory/deps/__init__.py +293 -0
- basic_memory/deps/config.py +26 -0
- basic_memory/deps/db.py +56 -0
- basic_memory/deps/importers.py +200 -0
- basic_memory/deps/projects.py +238 -0
- basic_memory/deps/repositories.py +179 -0
- basic_memory/deps/services.py +480 -0
- basic_memory/deps.py +14 -409
- basic_memory/file_utils.py +212 -3
- basic_memory/ignore_utils.py +5 -5
- basic_memory/importers/base.py +40 -19
- basic_memory/importers/chatgpt_importer.py +17 -4
- basic_memory/importers/claude_conversations_importer.py +27 -12
- basic_memory/importers/claude_projects_importer.py +50 -14
- basic_memory/importers/memory_json_importer.py +36 -16
- basic_memory/importers/utils.py +5 -2
- basic_memory/markdown/entity_parser.py +62 -23
- basic_memory/markdown/markdown_processor.py +67 -4
- basic_memory/markdown/plugins.py +4 -2
- basic_memory/markdown/utils.py +10 -1
- basic_memory/mcp/async_client.py +1 -0
- basic_memory/mcp/clients/__init__.py +28 -0
- basic_memory/mcp/clients/directory.py +70 -0
- basic_memory/mcp/clients/knowledge.py +176 -0
- basic_memory/mcp/clients/memory.py +120 -0
- basic_memory/mcp/clients/project.py +89 -0
- basic_memory/mcp/clients/resource.py +71 -0
- basic_memory/mcp/clients/search.py +65 -0
- basic_memory/mcp/container.py +110 -0
- basic_memory/mcp/project_context.py +47 -33
- basic_memory/mcp/prompts/ai_assistant_guide.py +2 -2
- basic_memory/mcp/prompts/recent_activity.py +2 -2
- basic_memory/mcp/prompts/utils.py +3 -3
- basic_memory/mcp/server.py +58 -0
- basic_memory/mcp/tools/build_context.py +14 -14
- basic_memory/mcp/tools/canvas.py +34 -12
- basic_memory/mcp/tools/chatgpt_tools.py +4 -1
- basic_memory/mcp/tools/delete_note.py +31 -7
- basic_memory/mcp/tools/edit_note.py +14 -9
- basic_memory/mcp/tools/list_directory.py +7 -17
- basic_memory/mcp/tools/move_note.py +35 -31
- basic_memory/mcp/tools/project_management.py +29 -25
- basic_memory/mcp/tools/read_content.py +13 -3
- basic_memory/mcp/tools/read_note.py +24 -14
- basic_memory/mcp/tools/recent_activity.py +32 -38
- basic_memory/mcp/tools/search.py +17 -10
- basic_memory/mcp/tools/utils.py +28 -0
- basic_memory/mcp/tools/view_note.py +2 -1
- basic_memory/mcp/tools/write_note.py +37 -14
- basic_memory/models/knowledge.py +15 -2
- basic_memory/models/project.py +7 -1
- basic_memory/models/search.py +58 -2
- basic_memory/project_resolver.py +222 -0
- basic_memory/repository/entity_repository.py +210 -3
- basic_memory/repository/observation_repository.py +1 -0
- basic_memory/repository/postgres_search_repository.py +451 -0
- basic_memory/repository/project_repository.py +38 -1
- basic_memory/repository/relation_repository.py +58 -2
- basic_memory/repository/repository.py +1 -0
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +77 -615
- basic_memory/repository/search_repository_base.py +241 -0
- basic_memory/repository/sqlite_search_repository.py +437 -0
- basic_memory/runtime.py +61 -0
- basic_memory/schemas/base.py +36 -6
- basic_memory/schemas/directory.py +2 -1
- basic_memory/schemas/memory.py +9 -2
- basic_memory/schemas/project_info.py +2 -0
- basic_memory/schemas/response.py +84 -27
- basic_memory/schemas/search.py +5 -0
- basic_memory/schemas/sync_report.py +1 -1
- basic_memory/schemas/v2/__init__.py +27 -0
- basic_memory/schemas/v2/entity.py +133 -0
- basic_memory/schemas/v2/resource.py +47 -0
- basic_memory/services/context_service.py +219 -43
- basic_memory/services/directory_service.py +26 -11
- basic_memory/services/entity_service.py +68 -33
- basic_memory/services/file_service.py +131 -16
- basic_memory/services/initialization.py +51 -26
- basic_memory/services/link_resolver.py +1 -0
- basic_memory/services/project_service.py +68 -43
- basic_memory/services/search_service.py +75 -16
- basic_memory/sync/__init__.py +2 -1
- basic_memory/sync/coordinator.py +160 -0
- basic_memory/sync/sync_service.py +135 -115
- basic_memory/sync/watch_service.py +32 -12
- basic_memory/telemetry.py +249 -0
- basic_memory/utils.py +96 -75
- {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/METADATA +129 -5
- basic_memory-0.17.4.dist-info/RECORD +193 -0
- {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
- basic_memory-0.16.1.dist-info/RECORD +0 -148
- {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
basic_memory/cli/commands/db.py
CHANGED
|
@@ -1,13 +1,50 @@
|
|
|
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
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from sqlalchemy.exc import OperationalError
|
|
7
10
|
|
|
8
11
|
from basic_memory import db
|
|
9
12
|
from basic_memory.cli.app import app
|
|
10
|
-
from basic_memory.config import ConfigManager
|
|
13
|
+
from basic_memory.config import ConfigManager
|
|
14
|
+
from basic_memory.repository import ProjectRepository
|
|
15
|
+
from basic_memory.services.initialization import reconcile_projects_with_config
|
|
16
|
+
from basic_memory.sync.sync_service import get_sync_service
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def _reindex_projects(app_config):
|
|
22
|
+
"""Reindex all projects in a single async context.
|
|
23
|
+
|
|
24
|
+
This ensures all database operations use the same event loop,
|
|
25
|
+
and proper cleanup happens when the function completes.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
await reconcile_projects_with_config(app_config)
|
|
29
|
+
|
|
30
|
+
# Get database session (migrations already run if needed)
|
|
31
|
+
_, session_maker = await db.get_or_create_db(
|
|
32
|
+
db_path=app_config.database_path,
|
|
33
|
+
db_type=db.DatabaseType.FILESYSTEM,
|
|
34
|
+
)
|
|
35
|
+
project_repository = ProjectRepository(session_maker)
|
|
36
|
+
projects = await project_repository.get_active_projects()
|
|
37
|
+
|
|
38
|
+
for project in projects:
|
|
39
|
+
console.print(f" Indexing [cyan]{project.name}[/cyan]...")
|
|
40
|
+
logger.info(f"Starting sync for project: {project.name}")
|
|
41
|
+
sync_service = await get_sync_service(project)
|
|
42
|
+
sync_dir = Path(project.path)
|
|
43
|
+
await sync_service.sync(sync_dir, project_name=project.name)
|
|
44
|
+
logger.info(f"Sync completed for project: {project.name}")
|
|
45
|
+
finally:
|
|
46
|
+
# Clean up database connections before event loop closes
|
|
47
|
+
await db.shutdown_db()
|
|
11
48
|
|
|
12
49
|
|
|
13
50
|
@app.command()
|
|
@@ -15,30 +52,52 @@ def reset(
|
|
|
15
52
|
reindex: bool = typer.Option(False, "--reindex", help="Rebuild db index from filesystem"),
|
|
16
53
|
): # pragma: no cover
|
|
17
54
|
"""Reset database (drop all tables and recreate)."""
|
|
18
|
-
|
|
55
|
+
console.print(
|
|
56
|
+
"[yellow]Note:[/yellow] This only deletes the index database. "
|
|
57
|
+
"Your markdown note files will not be affected.\n"
|
|
58
|
+
"Use [green]bm reset --reindex[/green] to automatically rebuild the index afterward."
|
|
59
|
+
)
|
|
60
|
+
if typer.confirm("Reset the database index?"):
|
|
19
61
|
logger.info("Resetting database...")
|
|
20
62
|
config_manager = ConfigManager()
|
|
21
63
|
app_config = config_manager.config
|
|
22
64
|
# Get database path
|
|
23
65
|
db_path = app_config.app_database_path
|
|
24
66
|
|
|
25
|
-
# Delete the database file if
|
|
26
|
-
|
|
27
|
-
db_path.
|
|
28
|
-
|
|
67
|
+
# Delete the database file and WAL files if they exist
|
|
68
|
+
for suffix in ["", "-shm", "-wal"]:
|
|
69
|
+
path = db_path.parent / f"{db_path.name}{suffix}"
|
|
70
|
+
if path.exists():
|
|
71
|
+
try:
|
|
72
|
+
path.unlink()
|
|
73
|
+
logger.info(f"Deleted: {path}")
|
|
74
|
+
except OSError as e:
|
|
75
|
+
console.print(
|
|
76
|
+
f"[red]Error:[/red] Cannot delete {path.name}: {e}\n"
|
|
77
|
+
"The database may be in use by another process (e.g., MCP server).\n"
|
|
78
|
+
"Please close Claude Desktop or any other Basic Memory clients and try again."
|
|
79
|
+
)
|
|
80
|
+
raise typer.Exit(1)
|
|
29
81
|
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
82
|
+
# Create a new empty database (preserves project configuration)
|
|
83
|
+
try:
|
|
84
|
+
asyncio.run(db.run_migrations(app_config))
|
|
85
|
+
except OperationalError as e:
|
|
86
|
+
if "disk I/O error" in str(e) or "database is locked" in str(e):
|
|
87
|
+
console.print(
|
|
88
|
+
"[red]Error:[/red] Cannot access database. "
|
|
89
|
+
"It may be in use by another process (e.g., MCP server).\n"
|
|
90
|
+
"Please close Claude Desktop or any other Basic Memory clients and try again."
|
|
91
|
+
)
|
|
92
|
+
raise typer.Exit(1)
|
|
93
|
+
raise
|
|
94
|
+
console.print("[green]Database reset complete[/green]")
|
|
38
95
|
|
|
39
96
|
if reindex:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
97
|
+
projects = list(app_config.projects)
|
|
98
|
+
if not projects:
|
|
99
|
+
console.print("[yellow]No projects configured. Skipping reindex.[/yellow]")
|
|
100
|
+
else:
|
|
101
|
+
console.print(f"Rebuilding search index for {len(projects)} project(s)...")
|
|
102
|
+
asyncio.run(_reindex_projects(app_config))
|
|
103
|
+
console.print("[green]Reindex complete[/green]")
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Format command for basic-memory CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated, Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
11
|
+
|
|
12
|
+
from basic_memory.cli.app import app
|
|
13
|
+
from basic_memory.config import ConfigManager, get_project_config
|
|
14
|
+
from basic_memory.file_utils import format_file
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_markdown_extension(path: Path) -> bool:
|
|
20
|
+
"""Check if file has a markdown extension."""
|
|
21
|
+
return path.suffix.lower() in (".md", ".markdown")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def format_single_file(file_path: Path, app_config) -> tuple[Path, bool, Optional[str]]:
|
|
25
|
+
"""Format a single file.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Tuple of (path, success, error_message)
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
result = await format_file(
|
|
32
|
+
file_path, app_config, is_markdown=is_markdown_extension(file_path)
|
|
33
|
+
)
|
|
34
|
+
if result is not None:
|
|
35
|
+
return (file_path, True, None)
|
|
36
|
+
else:
|
|
37
|
+
return (file_path, False, "No formatter configured or formatting skipped")
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return (file_path, False, str(e))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def format_files(
|
|
43
|
+
paths: list[Path], app_config, show_progress: bool = True
|
|
44
|
+
) -> tuple[int, int, list[tuple[Path, str]]]:
|
|
45
|
+
"""Format multiple files.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Tuple of (formatted_count, skipped_count, errors)
|
|
49
|
+
"""
|
|
50
|
+
formatted = 0
|
|
51
|
+
skipped = 0
|
|
52
|
+
errors: list[tuple[Path, str]] = []
|
|
53
|
+
|
|
54
|
+
if show_progress:
|
|
55
|
+
with Progress(
|
|
56
|
+
SpinnerColumn(),
|
|
57
|
+
TextColumn("[progress.description]{task.description}"),
|
|
58
|
+
console=console,
|
|
59
|
+
) as progress:
|
|
60
|
+
task = progress.add_task("Formatting files...", total=len(paths))
|
|
61
|
+
|
|
62
|
+
for file_path in paths:
|
|
63
|
+
path, success, error = await format_single_file(file_path, app_config)
|
|
64
|
+
if success:
|
|
65
|
+
formatted += 1
|
|
66
|
+
elif error and "No formatter configured" not in error:
|
|
67
|
+
errors.append((path, error))
|
|
68
|
+
else:
|
|
69
|
+
skipped += 1
|
|
70
|
+
progress.update(task, advance=1)
|
|
71
|
+
else:
|
|
72
|
+
for file_path in paths:
|
|
73
|
+
path, success, error = await format_single_file(file_path, app_config)
|
|
74
|
+
if success:
|
|
75
|
+
formatted += 1
|
|
76
|
+
elif error and "No formatter configured" not in error:
|
|
77
|
+
errors.append((path, error))
|
|
78
|
+
else:
|
|
79
|
+
skipped += 1
|
|
80
|
+
|
|
81
|
+
return formatted, skipped, errors
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def run_format(
|
|
85
|
+
path: Optional[Path] = None,
|
|
86
|
+
project: Optional[str] = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Run the format command."""
|
|
89
|
+
app_config = ConfigManager().config
|
|
90
|
+
|
|
91
|
+
# Check if formatting is enabled
|
|
92
|
+
if (
|
|
93
|
+
not app_config.format_on_save
|
|
94
|
+
and not app_config.formatter_command
|
|
95
|
+
and not app_config.formatters
|
|
96
|
+
):
|
|
97
|
+
console.print(
|
|
98
|
+
"[yellow]No formatters configured. Set format_on_save=true and "
|
|
99
|
+
"formatter_command or formatters in your config.[/yellow]"
|
|
100
|
+
)
|
|
101
|
+
console.print(
|
|
102
|
+
"\nExample config (~/.basic-memory/config.json):\n"
|
|
103
|
+
' "format_on_save": true,\n'
|
|
104
|
+
' "formatter_command": "prettier --write {file}"\n'
|
|
105
|
+
)
|
|
106
|
+
raise typer.Exit(1)
|
|
107
|
+
|
|
108
|
+
# Temporarily enable format_on_save for this command
|
|
109
|
+
# (so format_file actually runs the formatter)
|
|
110
|
+
original_format_on_save = app_config.format_on_save
|
|
111
|
+
app_config.format_on_save = True
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# Determine which files to format
|
|
115
|
+
if path:
|
|
116
|
+
# Format specific file or directory
|
|
117
|
+
if path.is_file():
|
|
118
|
+
files = [path]
|
|
119
|
+
elif path.is_dir():
|
|
120
|
+
# Find all markdown and json files
|
|
121
|
+
files = (
|
|
122
|
+
list(path.rglob("*.md"))
|
|
123
|
+
+ list(path.rglob("*.json"))
|
|
124
|
+
+ list(path.rglob("*.canvas"))
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
console.print(f"[red]Path not found: {path}[/red]")
|
|
128
|
+
raise typer.Exit(1)
|
|
129
|
+
else:
|
|
130
|
+
# Format all files in project
|
|
131
|
+
project_config = get_project_config(project)
|
|
132
|
+
project_path = Path(project_config.home)
|
|
133
|
+
|
|
134
|
+
if not project_path.exists():
|
|
135
|
+
console.print(f"[red]Project path not found: {project_path}[/red]")
|
|
136
|
+
raise typer.Exit(1)
|
|
137
|
+
|
|
138
|
+
# Find all markdown and json files
|
|
139
|
+
files = (
|
|
140
|
+
list(project_path.rglob("*.md"))
|
|
141
|
+
+ list(project_path.rglob("*.json"))
|
|
142
|
+
+ list(project_path.rglob("*.canvas"))
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if not files:
|
|
146
|
+
console.print("[yellow]No files found to format.[/yellow]")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
console.print(f"Found {len(files)} file(s) to format...")
|
|
150
|
+
|
|
151
|
+
formatted, skipped, errors = await format_files(files, app_config)
|
|
152
|
+
|
|
153
|
+
# Print summary
|
|
154
|
+
console.print()
|
|
155
|
+
if formatted > 0:
|
|
156
|
+
console.print(f"[green]Formatted: {formatted} file(s)[/green]")
|
|
157
|
+
if skipped > 0:
|
|
158
|
+
console.print(f"[dim]Skipped: {skipped} file(s) (no formatter for extension)[/dim]")
|
|
159
|
+
if errors:
|
|
160
|
+
console.print(f"[red]Errors: {len(errors)} file(s)[/red]")
|
|
161
|
+
for path, error in errors:
|
|
162
|
+
console.print(f" [red]{path}[/red]: {error}")
|
|
163
|
+
|
|
164
|
+
finally:
|
|
165
|
+
# Restore original setting
|
|
166
|
+
app_config.format_on_save = original_format_on_save
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@app.command()
|
|
170
|
+
def format(
|
|
171
|
+
path: Annotated[
|
|
172
|
+
Optional[Path],
|
|
173
|
+
typer.Argument(help="File or directory to format. Defaults to current project."),
|
|
174
|
+
] = None,
|
|
175
|
+
project: Annotated[
|
|
176
|
+
Optional[str],
|
|
177
|
+
typer.Option("--project", "-p", help="Project name to format."),
|
|
178
|
+
] = None,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Format files using configured formatters.
|
|
181
|
+
|
|
182
|
+
Uses the formatter_command or formatters settings from your config.
|
|
183
|
+
By default, formats all .md, .json, and .canvas files in the current project.
|
|
184
|
+
|
|
185
|
+
Examples:
|
|
186
|
+
basic-memory format # Format all files in current project
|
|
187
|
+
basic-memory format --project research # Format files in specific project
|
|
188
|
+
basic-memory format notes/meeting.md # Format a specific file
|
|
189
|
+
basic-memory format notes/ # Format all files in directory
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
asyncio.run(run_format(path, project))
|
|
193
|
+
except Exception as e:
|
|
194
|
+
if not isinstance(e, typer.Exit):
|
|
195
|
+
logger.error(f"Error formatting files: {e}")
|
|
196
|
+
console.print(f"[red]Error formatting files: {e}[/red]")
|
|
197
|
+
raise typer.Exit(code=1)
|
|
198
|
+
raise
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Annotated
|
|
6
|
+
from typing import Annotated, Tuple
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
9
|
from basic_memory.cli.app import import_app
|
|
10
|
-
from basic_memory.config import get_project_config
|
|
10
|
+
from basic_memory.config import ConfigManager, get_project_config
|
|
11
11
|
from basic_memory.importers import ChatGPTImporter
|
|
12
12
|
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
|
13
|
+
from basic_memory.services.file_service import FileService
|
|
13
14
|
from loguru import logger
|
|
14
15
|
from rich.console import Console
|
|
15
16
|
from rich.panel import Panel
|
|
@@ -17,11 +18,14 @@ from rich.panel import Panel
|
|
|
17
18
|
console = Console()
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
async def
|
|
21
|
-
"""Get MarkdownProcessor
|
|
21
|
+
async def get_importer_dependencies() -> Tuple[MarkdownProcessor, FileService]:
|
|
22
|
+
"""Get MarkdownProcessor and FileService instances for importers."""
|
|
22
23
|
config = get_project_config()
|
|
24
|
+
app_config = ConfigManager().config
|
|
23
25
|
entity_parser = EntityParser(config.home)
|
|
24
|
-
|
|
26
|
+
markdown_processor = MarkdownProcessor(entity_parser, app_config=app_config)
|
|
27
|
+
file_service = FileService(config.home, markdown_processor, app_config=app_config)
|
|
28
|
+
return markdown_processor, file_service
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
@import_app.command(name="chatgpt", help="Import conversations from ChatGPT JSON export.")
|
|
@@ -48,15 +52,15 @@ def import_chatgpt(
|
|
|
48
52
|
typer.echo(f"Error: File not found: {conversations_json}", err=True)
|
|
49
53
|
raise typer.Exit(1)
|
|
50
54
|
|
|
51
|
-
# Get
|
|
52
|
-
markdown_processor = asyncio.run(
|
|
55
|
+
# Get importer dependencies
|
|
56
|
+
markdown_processor, file_service = asyncio.run(get_importer_dependencies())
|
|
53
57
|
config = get_project_config()
|
|
54
58
|
# Process the file
|
|
55
59
|
base_path = config.home / folder
|
|
56
60
|
console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
|
|
57
61
|
|
|
58
62
|
# Create importer and run import
|
|
59
|
-
importer = ChatGPTImporter(config.home, markdown_processor)
|
|
63
|
+
importer = ChatGPTImporter(config.home, markdown_processor, file_service)
|
|
60
64
|
with conversations_json.open("r", encoding="utf-8") as file:
|
|
61
65
|
json_data = json.load(file)
|
|
62
66
|
result = asyncio.run(importer.import_data(json_data, folder))
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Annotated
|
|
6
|
+
from typing import Annotated, Tuple
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
9
|
from basic_memory.cli.app import claude_app
|
|
10
|
-
from basic_memory.config import get_project_config
|
|
10
|
+
from basic_memory.config import ConfigManager, get_project_config
|
|
11
11
|
from basic_memory.importers.claude_conversations_importer import ClaudeConversationsImporter
|
|
12
12
|
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
|
13
|
+
from basic_memory.services.file_service import FileService
|
|
13
14
|
from loguru import logger
|
|
14
15
|
from rich.console import Console
|
|
15
16
|
from rich.panel import Panel
|
|
@@ -17,11 +18,14 @@ from rich.panel import Panel
|
|
|
17
18
|
console = Console()
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
async def
|
|
21
|
-
"""Get MarkdownProcessor
|
|
21
|
+
async def get_importer_dependencies() -> Tuple[MarkdownProcessor, FileService]:
|
|
22
|
+
"""Get MarkdownProcessor and FileService instances for importers."""
|
|
22
23
|
config = get_project_config()
|
|
24
|
+
app_config = ConfigManager().config
|
|
23
25
|
entity_parser = EntityParser(config.home)
|
|
24
|
-
|
|
26
|
+
markdown_processor = MarkdownProcessor(entity_parser, app_config=app_config)
|
|
27
|
+
file_service = FileService(config.home, markdown_processor, app_config=app_config)
|
|
28
|
+
return markdown_processor, file_service
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
@claude_app.command(name="conversations", help="Import chat conversations from Claude.ai.")
|
|
@@ -49,11 +53,11 @@ def import_claude(
|
|
|
49
53
|
typer.echo(f"Error: File not found: {conversations_json}", err=True)
|
|
50
54
|
raise typer.Exit(1)
|
|
51
55
|
|
|
52
|
-
# Get
|
|
53
|
-
markdown_processor = asyncio.run(
|
|
56
|
+
# Get importer dependencies
|
|
57
|
+
markdown_processor, file_service = asyncio.run(get_importer_dependencies())
|
|
54
58
|
|
|
55
59
|
# Create the importer
|
|
56
|
-
importer = ClaudeConversationsImporter(config.home, markdown_processor)
|
|
60
|
+
importer = ClaudeConversationsImporter(config.home, markdown_processor, file_service)
|
|
57
61
|
|
|
58
62
|
# Process the file
|
|
59
63
|
base_path = config.home / folder
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Annotated
|
|
6
|
+
from typing import Annotated, Tuple
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
9
|
from basic_memory.cli.app import claude_app
|
|
10
|
-
from basic_memory.config import get_project_config
|
|
10
|
+
from basic_memory.config import ConfigManager, get_project_config
|
|
11
11
|
from basic_memory.importers.claude_projects_importer import ClaudeProjectsImporter
|
|
12
12
|
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
|
13
|
+
from basic_memory.services.file_service import FileService
|
|
13
14
|
from loguru import logger
|
|
14
15
|
from rich.console import Console
|
|
15
16
|
from rich.panel import Panel
|
|
@@ -17,11 +18,14 @@ from rich.panel import Panel
|
|
|
17
18
|
console = Console()
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
async def
|
|
21
|
-
"""Get MarkdownProcessor
|
|
21
|
+
async def get_importer_dependencies() -> Tuple[MarkdownProcessor, FileService]:
|
|
22
|
+
"""Get MarkdownProcessor and FileService instances for importers."""
|
|
22
23
|
config = get_project_config()
|
|
24
|
+
app_config = ConfigManager().config
|
|
23
25
|
entity_parser = EntityParser(config.home)
|
|
24
|
-
|
|
26
|
+
markdown_processor = MarkdownProcessor(entity_parser, app_config=app_config)
|
|
27
|
+
file_service = FileService(config.home, markdown_processor, app_config=app_config)
|
|
28
|
+
return markdown_processor, file_service
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
@claude_app.command(name="projects", help="Import projects from Claude.ai.")
|
|
@@ -48,11 +52,11 @@ def import_projects(
|
|
|
48
52
|
typer.echo(f"Error: File not found: {projects_json}", err=True)
|
|
49
53
|
raise typer.Exit(1)
|
|
50
54
|
|
|
51
|
-
# Get
|
|
52
|
-
markdown_processor = asyncio.run(
|
|
55
|
+
# Get importer dependencies
|
|
56
|
+
markdown_processor, file_service = asyncio.run(get_importer_dependencies())
|
|
53
57
|
|
|
54
58
|
# Create the importer
|
|
55
|
-
importer = ClaudeProjectsImporter(config.home, markdown_processor)
|
|
59
|
+
importer = ClaudeProjectsImporter(config.home, markdown_processor, file_service)
|
|
56
60
|
|
|
57
61
|
# Process the file
|
|
58
62
|
base_path = config.home / base_folder if base_folder else config.home
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Annotated
|
|
6
|
+
from typing import Annotated, Tuple
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
9
|
from basic_memory.cli.app import import_app
|
|
10
|
-
from basic_memory.config import get_project_config
|
|
10
|
+
from basic_memory.config import ConfigManager, get_project_config
|
|
11
11
|
from basic_memory.importers.memory_json_importer import MemoryJsonImporter
|
|
12
12
|
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
|
13
|
+
from basic_memory.services.file_service import FileService
|
|
13
14
|
from loguru import logger
|
|
14
15
|
from rich.console import Console
|
|
15
16
|
from rich.panel import Panel
|
|
@@ -17,11 +18,14 @@ from rich.panel import Panel
|
|
|
17
18
|
console = Console()
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
async def
|
|
21
|
-
"""Get MarkdownProcessor
|
|
21
|
+
async def get_importer_dependencies() -> Tuple[MarkdownProcessor, FileService]:
|
|
22
|
+
"""Get MarkdownProcessor and FileService instances for importers."""
|
|
22
23
|
config = get_project_config()
|
|
24
|
+
app_config = ConfigManager().config
|
|
23
25
|
entity_parser = EntityParser(config.home)
|
|
24
|
-
|
|
26
|
+
markdown_processor = MarkdownProcessor(entity_parser, app_config=app_config)
|
|
27
|
+
file_service = FileService(config.home, markdown_processor, app_config=app_config)
|
|
28
|
+
return markdown_processor, file_service
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
@import_app.command()
|
|
@@ -47,11 +51,11 @@ def memory_json(
|
|
|
47
51
|
|
|
48
52
|
config = get_project_config()
|
|
49
53
|
try:
|
|
50
|
-
# Get
|
|
51
|
-
markdown_processor = asyncio.run(
|
|
54
|
+
# Get importer dependencies
|
|
55
|
+
markdown_processor, file_service = asyncio.run(get_importer_dependencies())
|
|
52
56
|
|
|
53
57
|
# Create the importer
|
|
54
|
-
importer = MemoryJsonImporter(config.home, markdown_processor)
|
|
58
|
+
importer = MemoryJsonImporter(config.home, markdown_processor, file_service)
|
|
55
59
|
|
|
56
60
|
# Process the file
|
|
57
61
|
base_path = config.home if not destination_folder else config.home / destination_folder
|
basic_memory/cli/commands/mcp.py
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
"""MCP server command with streamable HTTP transport."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import os
|
|
5
4
|
import typer
|
|
6
5
|
from typing import Optional
|
|
7
6
|
|
|
8
7
|
from basic_memory.cli.app import app
|
|
9
|
-
from basic_memory.config import ConfigManager
|
|
8
|
+
from basic_memory.config import ConfigManager, init_mcp_logging
|
|
10
9
|
|
|
11
|
-
# Import mcp instance
|
|
10
|
+
# Import mcp instance (has lifespan that handles initialization and file sync)
|
|
12
11
|
from basic_memory.mcp.server import mcp as mcp_server # pragma: no cover
|
|
13
12
|
|
|
14
13
|
# Import mcp tools to register them
|
|
@@ -17,8 +16,6 @@ import basic_memory.mcp.tools # noqa: F401 # pragma: no cover
|
|
|
17
16
|
# Import prompts to register them
|
|
18
17
|
import basic_memory.mcp.prompts # noqa: F401 # pragma: no cover
|
|
19
18
|
from loguru import logger
|
|
20
|
-
import threading
|
|
21
|
-
from basic_memory.services.initialization import initialize_file_sync
|
|
22
19
|
|
|
23
20
|
config = ConfigManager().config
|
|
24
21
|
|
|
@@ -43,7 +40,11 @@ if not config.cloud_mode_enabled:
|
|
|
43
40
|
- stdio: Standard I/O (good for local usage)
|
|
44
41
|
- streamable-http: Recommended for web deployments (default)
|
|
45
42
|
- sse: Server-Sent Events (for compatibility with existing clients)
|
|
43
|
+
|
|
44
|
+
Initialization, file sync, and cleanup are handled by the MCP server's lifespan.
|
|
46
45
|
"""
|
|
46
|
+
# Initialize logging for MCP (file only, stdout breaks protocol)
|
|
47
|
+
init_mcp_logging()
|
|
47
48
|
|
|
48
49
|
# Validate and set project constraint if specified
|
|
49
50
|
if project:
|
|
@@ -57,27 +58,8 @@ if not config.cloud_mode_enabled:
|
|
|
57
58
|
os.environ["BASIC_MEMORY_MCP_PROJECT"] = project_name
|
|
58
59
|
logger.info(f"MCP server constrained to project: {project_name}")
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def run_file_sync():
|
|
63
|
-
"""Run file sync in a separate thread with its own event loop."""
|
|
64
|
-
loop = asyncio.new_event_loop()
|
|
65
|
-
asyncio.set_event_loop(loop)
|
|
66
|
-
try:
|
|
67
|
-
loop.run_until_complete(initialize_file_sync(app_config))
|
|
68
|
-
except Exception as e:
|
|
69
|
-
logger.error(f"File sync error: {e}", err=True)
|
|
70
|
-
finally:
|
|
71
|
-
loop.close()
|
|
72
|
-
|
|
73
|
-
logger.info(f"Sync changes enabled: {app_config.sync_changes}")
|
|
74
|
-
if app_config.sync_changes:
|
|
75
|
-
# Start the sync thread
|
|
76
|
-
sync_thread = threading.Thread(target=run_file_sync, daemon=True)
|
|
77
|
-
sync_thread.start()
|
|
78
|
-
logger.info("Started file sync in background")
|
|
79
|
-
|
|
80
|
-
# Now run the MCP server (blocks)
|
|
61
|
+
# Run the MCP server (blocks)
|
|
62
|
+
# Lifespan handles: initialization, migrations, file sync, cleanup
|
|
81
63
|
logger.info(f"Starting MCP server with {transport.upper()} transport")
|
|
82
64
|
|
|
83
65
|
if transport == "stdio":
|
|
@@ -16,14 +16,9 @@ from datetime import datetime
|
|
|
16
16
|
|
|
17
17
|
from rich.panel import Panel
|
|
18
18
|
from basic_memory.mcp.async_client import get_client
|
|
19
|
-
from basic_memory.mcp.tools.utils import call_get
|
|
20
|
-
from basic_memory.schemas.project_info import ProjectList
|
|
21
|
-
from basic_memory.mcp.tools.utils import call_post
|
|
22
|
-
from basic_memory.schemas.project_info import ProjectStatusResponse
|
|
23
|
-
from basic_memory.mcp.tools.utils import call_delete
|
|
24
|
-
from basic_memory.mcp.tools.utils import call_put
|
|
19
|
+
from basic_memory.mcp.tools.utils import call_get, call_post, call_delete, call_put, call_patch
|
|
20
|
+
from basic_memory.schemas.project_info import ProjectList, ProjectStatusResponse
|
|
25
21
|
from basic_memory.utils import generate_permalink, normalize_project_path
|
|
26
|
-
from basic_memory.mcp.tools.utils import call_patch
|
|
27
22
|
|
|
28
23
|
# Import rclone commands for project sync
|
|
29
24
|
from basic_memory.cli.commands.cloud.rclone_commands import (
|
|
@@ -254,9 +249,17 @@ def remove_project(
|
|
|
254
249
|
|
|
255
250
|
async def _remove_project():
|
|
256
251
|
async with get_client() as client:
|
|
252
|
+
# Convert name to permalink for efficient resolution
|
|
257
253
|
project_permalink = generate_permalink(name)
|
|
254
|
+
|
|
255
|
+
# Use v2 project resolver to find project ID by permalink
|
|
256
|
+
resolve_data = {"identifier": project_permalink}
|
|
257
|
+
response = await call_post(client, "/v2/projects/resolve", json=resolve_data)
|
|
258
|
+
target_project = response.json()
|
|
259
|
+
|
|
260
|
+
# Use v2 API with project ID
|
|
258
261
|
response = await call_delete(
|
|
259
|
-
client, f"/projects/{
|
|
262
|
+
client, f"/v2/projects/{target_project['external_id']}?delete_notes={delete_notes}"
|
|
260
263
|
)
|
|
261
264
|
return ProjectStatusResponse.model_validate(response.json())
|
|
262
265
|
|
|
@@ -329,8 +332,18 @@ def set_default_project(
|
|
|
329
332
|
|
|
330
333
|
async def _set_default():
|
|
331
334
|
async with get_client() as client:
|
|
335
|
+
# Convert name to permalink for efficient resolution
|
|
332
336
|
project_permalink = generate_permalink(name)
|
|
333
|
-
|
|
337
|
+
|
|
338
|
+
# Use v2 project resolver to find project ID by permalink
|
|
339
|
+
resolve_data = {"identifier": project_permalink}
|
|
340
|
+
response = await call_post(client, "/v2/projects/resolve", json=resolve_data)
|
|
341
|
+
target_project = response.json()
|
|
342
|
+
|
|
343
|
+
# Use v2 API with project ID
|
|
344
|
+
response = await call_put(
|
|
345
|
+
client, f"/v2/projects/{target_project['external_id']}/default"
|
|
346
|
+
)
|
|
334
347
|
return ProjectStatusResponse.model_validate(response.json())
|
|
335
348
|
|
|
336
349
|
try:
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Status command for basic-memory CLI."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
from typing import Set, Dict
|
|
5
4
|
from typing import Annotated, Optional
|
|
6
5
|
|
|
@@ -165,8 +164,10 @@ def status(
|
|
|
165
164
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed file information"),
|
|
166
165
|
):
|
|
167
166
|
"""Show sync status between files and database."""
|
|
167
|
+
from basic_memory.cli.commands.command_utils import run_with_cleanup
|
|
168
|
+
|
|
168
169
|
try:
|
|
169
|
-
|
|
170
|
+
run_with_cleanup(run_status(project, verbose)) # pragma: no cover
|
|
170
171
|
except Exception as e:
|
|
171
172
|
logger.error(f"Error checking status: {e}")
|
|
172
173
|
typer.echo(f"Error checking status: {e}", err=True)
|