basic-memory 0.14.4__py3-none-any.whl → 0.15.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of basic-memory might be problematic. Click here for more details.
- basic_memory/__init__.py +1 -1
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +5 -9
- basic_memory/api/app.py +10 -4
- basic_memory/api/routers/directory_router.py +23 -2
- basic_memory/api/routers/knowledge_router.py +25 -8
- basic_memory/api/routers/project_router.py +100 -4
- basic_memory/cli/app.py +9 -28
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/cloud/__init__.py +5 -0
- basic_memory/cli/commands/cloud/api_client.py +112 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +818 -0
- basic_memory/cli/commands/cloud/core_commands.py +288 -0
- basic_memory/cli/commands/cloud/mount_commands.py +295 -0
- basic_memory/cli/commands/cloud/rclone_config.py +288 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +198 -0
- basic_memory/cli/commands/command_utils.py +43 -0
- basic_memory/cli/commands/import_memory_json.py +0 -4
- basic_memory/cli/commands/mcp.py +77 -60
- basic_memory/cli/commands/project.py +154 -152
- basic_memory/cli/commands/status.py +25 -22
- basic_memory/cli/commands/sync.py +45 -228
- basic_memory/cli/commands/tool.py +87 -16
- basic_memory/cli/main.py +1 -0
- basic_memory/config.py +131 -21
- basic_memory/db.py +104 -3
- basic_memory/deps.py +27 -8
- basic_memory/file_utils.py +37 -13
- basic_memory/ignore_utils.py +295 -0
- basic_memory/markdown/plugins.py +9 -7
- basic_memory/mcp/async_client.py +124 -14
- basic_memory/mcp/project_context.py +141 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +49 -4
- basic_memory/mcp/prompts/continue_conversation.py +17 -16
- basic_memory/mcp/prompts/recent_activity.py +116 -32
- basic_memory/mcp/prompts/search.py +13 -12
- basic_memory/mcp/prompts/utils.py +11 -4
- basic_memory/mcp/resources/ai_assistant_guide.md +211 -341
- basic_memory/mcp/resources/project_info.py +27 -11
- basic_memory/mcp/server.py +0 -37
- basic_memory/mcp/tools/__init__.py +5 -6
- basic_memory/mcp/tools/build_context.py +67 -56
- basic_memory/mcp/tools/canvas.py +38 -26
- basic_memory/mcp/tools/chatgpt_tools.py +187 -0
- basic_memory/mcp/tools/delete_note.py +81 -47
- basic_memory/mcp/tools/edit_note.py +155 -138
- basic_memory/mcp/tools/list_directory.py +112 -99
- basic_memory/mcp/tools/move_note.py +181 -101
- basic_memory/mcp/tools/project_management.py +113 -277
- basic_memory/mcp/tools/read_content.py +91 -74
- basic_memory/mcp/tools/read_note.py +152 -115
- basic_memory/mcp/tools/recent_activity.py +471 -68
- basic_memory/mcp/tools/search.py +105 -92
- basic_memory/mcp/tools/sync_status.py +136 -130
- basic_memory/mcp/tools/utils.py +4 -0
- basic_memory/mcp/tools/view_note.py +44 -33
- basic_memory/mcp/tools/write_note.py +151 -90
- basic_memory/models/knowledge.py +12 -6
- basic_memory/models/project.py +6 -2
- basic_memory/repository/entity_repository.py +89 -82
- basic_memory/repository/relation_repository.py +13 -0
- basic_memory/repository/repository.py +18 -5
- basic_memory/repository/search_repository.py +46 -2
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/base.py +39 -11
- basic_memory/schemas/cloud.py +46 -0
- basic_memory/schemas/memory.py +90 -21
- basic_memory/schemas/project_info.py +9 -10
- basic_memory/schemas/sync_report.py +48 -0
- basic_memory/services/context_service.py +25 -11
- basic_memory/services/directory_service.py +124 -3
- basic_memory/services/entity_service.py +100 -48
- basic_memory/services/initialization.py +30 -11
- basic_memory/services/project_service.py +101 -24
- basic_memory/services/search_service.py +16 -8
- basic_memory/sync/sync_service.py +173 -34
- basic_memory/sync/watch_service.py +101 -40
- basic_memory/utils.py +14 -4
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/METADATA +57 -9
- basic_memory-0.15.1.dist-info/RECORD +146 -0
- basic_memory/mcp/project_session.py +0 -120
- basic_memory-0.14.4.dist-info/RECORD +0 -133
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/WHEEL +0 -0
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/licenses/LICENSE +0 -0
basic_memory/cli/commands/mcp.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""MCP server command with streamable HTTP transport."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import os
|
|
4
5
|
import typer
|
|
6
|
+
from typing import Optional
|
|
5
7
|
|
|
6
8
|
from basic_memory.cli.app import app
|
|
7
9
|
from basic_memory.config import ConfigManager
|
|
@@ -15,63 +17,78 @@ import basic_memory.mcp.tools # noqa: F401 # pragma: no cover
|
|
|
15
17
|
# Import prompts to register them
|
|
16
18
|
import basic_memory.mcp.prompts # noqa: F401 # pragma: no cover
|
|
17
19
|
from loguru import logger
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
20
|
+
import threading
|
|
21
|
+
from basic_memory.services.initialization import initialize_file_sync
|
|
22
|
+
|
|
23
|
+
config = ConfigManager().config
|
|
24
|
+
|
|
25
|
+
if not config.cloud_mode_enabled:
|
|
26
|
+
|
|
27
|
+
@app.command()
|
|
28
|
+
def mcp(
|
|
29
|
+
transport: str = typer.Option(
|
|
30
|
+
"stdio", help="Transport type: stdio, streamable-http, or sse"
|
|
31
|
+
),
|
|
32
|
+
host: str = typer.Option(
|
|
33
|
+
"0.0.0.0", help="Host for HTTP transports (use 0.0.0.0 to allow external connections)"
|
|
34
|
+
),
|
|
35
|
+
port: int = typer.Option(8000, help="Port for HTTP transports"),
|
|
36
|
+
path: str = typer.Option("/mcp", help="Path prefix for streamable-http transport"),
|
|
37
|
+
project: Optional[str] = typer.Option(None, help="Restrict MCP server to single project"),
|
|
38
|
+
): # pragma: no cover
|
|
39
|
+
"""Run the MCP server with configurable transport options.
|
|
40
|
+
|
|
41
|
+
This command starts an MCP server using one of three transport options:
|
|
42
|
+
|
|
43
|
+
- stdio: Standard I/O (good for local usage)
|
|
44
|
+
- streamable-http: Recommended for web deployments (default)
|
|
45
|
+
- sse: Server-Sent Events (for compatibility with existing clients)
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
# Validate and set project constraint if specified
|
|
49
|
+
if project:
|
|
50
|
+
config_manager = ConfigManager()
|
|
51
|
+
project_name, _ = config_manager.get_project(project)
|
|
52
|
+
if not project_name:
|
|
53
|
+
typer.echo(f"No project found named: {project}", err=True)
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
|
|
56
|
+
# Set env var with validated project name
|
|
57
|
+
os.environ["BASIC_MEMORY_MCP_PROJECT"] = project_name
|
|
58
|
+
logger.info(f"MCP server constrained to project: {project_name}")
|
|
59
|
+
|
|
60
|
+
app_config = ConfigManager().config
|
|
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)
|
|
81
|
+
logger.info(f"Starting MCP server with {transport.upper()} transport")
|
|
82
|
+
|
|
83
|
+
if transport == "stdio":
|
|
84
|
+
mcp_server.run(
|
|
85
|
+
transport=transport,
|
|
86
|
+
)
|
|
87
|
+
elif transport == "streamable-http" or transport == "sse":
|
|
88
|
+
mcp_server.run(
|
|
89
|
+
transport=transport,
|
|
90
|
+
host=host,
|
|
91
|
+
port=port,
|
|
92
|
+
path=path,
|
|
93
|
+
log_level="INFO",
|
|
94
|
+
)
|
|
@@ -9,14 +9,13 @@ from rich.console import Console
|
|
|
9
9
|
from rich.table import Table
|
|
10
10
|
|
|
11
11
|
from basic_memory.cli.app import app
|
|
12
|
-
from basic_memory.
|
|
13
|
-
from basic_memory.
|
|
12
|
+
from basic_memory.cli.commands.command_utils import get_project_info
|
|
13
|
+
from basic_memory.config import ConfigManager
|
|
14
14
|
import json
|
|
15
15
|
from datetime import datetime
|
|
16
16
|
|
|
17
17
|
from rich.panel import Panel
|
|
18
|
-
from
|
|
19
|
-
from basic_memory.mcp.async_client import client
|
|
18
|
+
from basic_memory.mcp.async_client import get_client
|
|
20
19
|
from basic_memory.mcp.tools.utils import call_get
|
|
21
20
|
from basic_memory.schemas.project_info import ProjectList
|
|
22
21
|
from basic_memory.mcp.tools.utils import call_post
|
|
@@ -32,6 +31,8 @@ console = Console()
|
|
|
32
31
|
project_app = typer.Typer(help="Manage multiple Basic Memory projects")
|
|
33
32
|
app.add_typer(project_app, name="project")
|
|
34
33
|
|
|
34
|
+
config = ConfigManager().config
|
|
35
|
+
|
|
35
36
|
|
|
36
37
|
def format_path(path: str) -> str:
|
|
37
38
|
"""Format a path for display, using ~ for home directory."""
|
|
@@ -43,22 +44,24 @@ def format_path(path: str) -> str:
|
|
|
43
44
|
|
|
44
45
|
@project_app.command("list")
|
|
45
46
|
def list_projects() -> None:
|
|
46
|
-
"""List all
|
|
47
|
-
|
|
47
|
+
"""List all Basic Memory projects."""
|
|
48
|
+
|
|
49
|
+
async def _list_projects():
|
|
50
|
+
async with get_client() as client:
|
|
51
|
+
response = await call_get(client, "/projects/projects")
|
|
52
|
+
return ProjectList.model_validate(response.json())
|
|
53
|
+
|
|
48
54
|
try:
|
|
49
|
-
|
|
50
|
-
result = ProjectList.model_validate(response.json())
|
|
55
|
+
result = asyncio.run(_list_projects())
|
|
51
56
|
|
|
52
57
|
table = Table(title="Basic Memory Projects")
|
|
53
58
|
table.add_column("Name", style="cyan")
|
|
54
59
|
table.add_column("Path", style="green")
|
|
55
|
-
table.add_column("Default", style="
|
|
56
|
-
table.add_column("Active", style="magenta")
|
|
60
|
+
table.add_column("Default", style="magenta")
|
|
57
61
|
|
|
58
62
|
for project in result.projects:
|
|
59
63
|
is_default = "✓" if project.is_default else ""
|
|
60
|
-
|
|
61
|
-
table.add_row(project.name, format_path(project.path), is_default, is_active)
|
|
64
|
+
table.add_row(project.name, format_path(project.path), is_default)
|
|
62
65
|
|
|
63
66
|
console.print(table)
|
|
64
67
|
except Exception as e:
|
|
@@ -66,44 +69,75 @@ def list_projects() -> None:
|
|
|
66
69
|
raise typer.Exit(1)
|
|
67
70
|
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
72
|
+
if config.cloud_mode_enabled:
|
|
73
|
+
|
|
74
|
+
@project_app.command("add")
|
|
75
|
+
def add_project_cloud(
|
|
76
|
+
name: str = typer.Argument(..., help="Name of the project"),
|
|
77
|
+
set_default: bool = typer.Option(False, "--default", help="Set as default project"),
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Add a new project to Basic Memory Cloud"""
|
|
80
|
+
|
|
81
|
+
async def _add_project():
|
|
82
|
+
async with get_client() as client:
|
|
83
|
+
data = {"name": name, "path": generate_permalink(name), "set_default": set_default}
|
|
84
|
+
response = await call_post(client, "/projects/projects", json=data)
|
|
85
|
+
return ProjectStatusResponse.model_validate(response.json())
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
result = asyncio.run(_add_project())
|
|
89
|
+
console.print(f"[green]{result.message}[/green]")
|
|
90
|
+
except Exception as e:
|
|
91
|
+
console.print(f"[red]Error adding project: {str(e)}[/red]")
|
|
92
|
+
raise typer.Exit(1)
|
|
93
|
+
|
|
94
|
+
# Display usage hint
|
|
95
|
+
console.print("\nTo use this project:")
|
|
96
|
+
console.print(f" basic-memory --project={name} <command>")
|
|
97
|
+
else:
|
|
98
|
+
|
|
99
|
+
@project_app.command("add")
|
|
100
|
+
def add_project(
|
|
101
|
+
name: str = typer.Argument(..., help="Name of the project"),
|
|
102
|
+
path: str = typer.Argument(..., help="Path to the project directory"),
|
|
103
|
+
set_default: bool = typer.Option(False, "--default", help="Set as default project"),
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Add a new project."""
|
|
106
|
+
# Resolve to absolute path
|
|
107
|
+
resolved_path = Path(os.path.abspath(os.path.expanduser(path))).as_posix()
|
|
108
|
+
|
|
109
|
+
async def _add_project():
|
|
110
|
+
async with get_client() as client:
|
|
111
|
+
data = {"name": name, "path": resolved_path, "set_default": set_default}
|
|
112
|
+
response = await call_post(client, "/projects/projects", json=data)
|
|
113
|
+
return ProjectStatusResponse.model_validate(response.json())
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
result = asyncio.run(_add_project())
|
|
117
|
+
console.print(f"[green]{result.message}[/green]")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
console.print(f"[red]Error adding project: {str(e)}[/red]")
|
|
120
|
+
raise typer.Exit(1)
|
|
121
|
+
|
|
122
|
+
# Display usage hint
|
|
123
|
+
console.print("\nTo use this project:")
|
|
124
|
+
console.print(f" basic-memory --project={name} <command>")
|
|
95
125
|
|
|
96
126
|
|
|
97
127
|
@project_app.command("remove")
|
|
98
128
|
def remove_project(
|
|
99
129
|
name: str = typer.Argument(..., help="Name of the project to remove"),
|
|
100
130
|
) -> None:
|
|
101
|
-
"""Remove a project
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
131
|
+
"""Remove a project."""
|
|
132
|
+
|
|
133
|
+
async def _remove_project():
|
|
134
|
+
async with get_client() as client:
|
|
135
|
+
project_permalink = generate_permalink(name)
|
|
136
|
+
response = await call_delete(client, f"/projects/{project_permalink}")
|
|
137
|
+
return ProjectStatusResponse.model_validate(response.json())
|
|
106
138
|
|
|
139
|
+
try:
|
|
140
|
+
result = asyncio.run(_remove_project())
|
|
107
141
|
console.print(f"[green]{result.message}[/green]")
|
|
108
142
|
except Exception as e:
|
|
109
143
|
console.print(f"[red]Error removing project: {str(e)}[/red]")
|
|
@@ -113,100 +147,104 @@ def remove_project(
|
|
|
113
147
|
console.print("[yellow]Note: The project files have not been deleted from disk.[/yellow]")
|
|
114
148
|
|
|
115
149
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
150
|
+
if not config.cloud_mode_enabled:
|
|
151
|
+
|
|
152
|
+
@project_app.command("default")
|
|
153
|
+
def set_default_project(
|
|
154
|
+
name: str = typer.Argument(..., help="Name of the project to set as CLI default"),
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Set the default project when 'config.default_project_mode' is set."""
|
|
157
|
+
|
|
158
|
+
async def _set_default():
|
|
159
|
+
async with get_client() as client:
|
|
160
|
+
project_permalink = generate_permalink(name)
|
|
161
|
+
response = await call_put(client, f"/projects/{project_permalink}/default")
|
|
162
|
+
return ProjectStatusResponse.model_validate(response.json())
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
result = asyncio.run(_set_default())
|
|
166
|
+
console.print(f"[green]{result.message}[/green]")
|
|
167
|
+
except Exception as e:
|
|
168
|
+
console.print(f"[red]Error setting default project: {str(e)}[/red]")
|
|
169
|
+
raise typer.Exit(1)
|
|
170
|
+
|
|
171
|
+
@project_app.command("sync-config")
|
|
172
|
+
def synchronize_projects() -> None:
|
|
173
|
+
"""Synchronize project config between configuration file and database."""
|
|
174
|
+
|
|
175
|
+
async def _sync_config():
|
|
176
|
+
async with get_client() as client:
|
|
177
|
+
response = await call_post(client, "/projects/config/sync")
|
|
178
|
+
return ProjectStatusResponse.model_validate(response.json())
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
result = asyncio.run(_sync_config())
|
|
182
|
+
console.print(f"[green]{result.message}[/green]")
|
|
183
|
+
except Exception as e: # pragma: no cover
|
|
184
|
+
console.print(f"[red]Error synchronizing projects: {str(e)}[/red]")
|
|
185
|
+
raise typer.Exit(1)
|
|
186
|
+
|
|
187
|
+
@project_app.command("move")
|
|
188
|
+
def move_project(
|
|
189
|
+
name: str = typer.Argument(..., help="Name of the project to move"),
|
|
190
|
+
new_path: str = typer.Argument(..., help="New absolute path for the project"),
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Move a project to a new location."""
|
|
193
|
+
# Resolve to absolute path
|
|
194
|
+
resolved_path = Path(os.path.abspath(os.path.expanduser(new_path))).as_posix()
|
|
195
|
+
|
|
196
|
+
async def _move_project():
|
|
197
|
+
async with get_client() as client:
|
|
198
|
+
data = {"path": resolved_path}
|
|
199
|
+
project_permalink = generate_permalink(name)
|
|
200
|
+
|
|
201
|
+
# TODO fix route to use ProjectPathDep
|
|
202
|
+
response = await call_patch(
|
|
203
|
+
client, f"/{name}/project/{project_permalink}", json=data
|
|
204
|
+
)
|
|
205
|
+
return ProjectStatusResponse.model_validate(response.json())
|
|
169
206
|
|
|
170
|
-
|
|
207
|
+
try:
|
|
208
|
+
result = asyncio.run(_move_project())
|
|
209
|
+
console.print(f"[green]{result.message}[/green]")
|
|
171
210
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
211
|
+
# Show important file movement reminder
|
|
212
|
+
console.print() # Empty line for spacing
|
|
213
|
+
console.print(
|
|
214
|
+
Panel(
|
|
215
|
+
"[bold red]IMPORTANT:[/bold red] Project configuration updated successfully.\n\n"
|
|
216
|
+
"[yellow]You must manually move your project files from the old location to:[/yellow]\n"
|
|
217
|
+
f"[cyan]{resolved_path}[/cyan]\n\n"
|
|
218
|
+
"[dim]Basic Memory has only updated the configuration - your files remain in their original location.[/dim]",
|
|
219
|
+
title="⚠️ Manual File Movement Required",
|
|
220
|
+
border_style="yellow",
|
|
221
|
+
expand=False,
|
|
222
|
+
)
|
|
183
223
|
)
|
|
184
|
-
)
|
|
185
224
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
225
|
+
except Exception as e:
|
|
226
|
+
console.print(f"[red]Error moving project: {str(e)}[/red]")
|
|
227
|
+
raise typer.Exit(1)
|
|
189
228
|
|
|
190
229
|
|
|
191
230
|
@project_app.command("info")
|
|
192
231
|
def display_project_info(
|
|
232
|
+
name: str = typer.Argument(..., help="Name of the project"),
|
|
193
233
|
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
194
234
|
):
|
|
195
235
|
"""Display detailed information and statistics about the current project."""
|
|
196
236
|
try:
|
|
197
237
|
# Get project info
|
|
198
|
-
info = asyncio.run(
|
|
238
|
+
info = asyncio.run(get_project_info(name))
|
|
199
239
|
|
|
200
240
|
if json_output:
|
|
201
241
|
# Convert to JSON and print
|
|
202
242
|
print(json.dumps(info.model_dump(), indent=2, default=str))
|
|
203
243
|
else:
|
|
204
|
-
# Create rich display
|
|
205
|
-
console = Console()
|
|
206
|
-
|
|
207
244
|
# Project configuration section
|
|
208
245
|
console.print(
|
|
209
246
|
Panel(
|
|
247
|
+
f"Basic Memory version: [bold green]{info.system.version}[/bold green]\n"
|
|
210
248
|
f"[bold]Project:[/bold] {info.project_name}\n"
|
|
211
249
|
f"[bold]Path:[/bold] {info.project_path}\n"
|
|
212
250
|
f"[bold]Default Project:[/bold] {info.default_project}\n",
|
|
@@ -276,42 +314,6 @@ def display_project_info(
|
|
|
276
314
|
|
|
277
315
|
console.print(recent_table)
|
|
278
316
|
|
|
279
|
-
# System status
|
|
280
|
-
system_tree = Tree("🖥️ System Status")
|
|
281
|
-
system_tree.add(f"Basic Memory version: [bold green]{info.system.version}[/bold green]")
|
|
282
|
-
system_tree.add(
|
|
283
|
-
f"Database: [cyan]{info.system.database_path}[/cyan] ([green]{info.system.database_size}[/green])"
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
# Watch status
|
|
287
|
-
if info.system.watch_status: # pragma: no cover
|
|
288
|
-
watch_branch = system_tree.add("Watch Service")
|
|
289
|
-
running = info.system.watch_status.get("running", False)
|
|
290
|
-
status_color = "green" if running else "red"
|
|
291
|
-
watch_branch.add(
|
|
292
|
-
f"Status: [bold {status_color}]{'Running' if running else 'Stopped'}[/bold {status_color}]"
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
if running:
|
|
296
|
-
start_time = (
|
|
297
|
-
datetime.fromisoformat(info.system.watch_status.get("start_time", ""))
|
|
298
|
-
if isinstance(info.system.watch_status.get("start_time"), str)
|
|
299
|
-
else info.system.watch_status.get("start_time")
|
|
300
|
-
)
|
|
301
|
-
watch_branch.add(
|
|
302
|
-
f"Running since: [cyan]{start_time.strftime('%Y-%m-%d %H:%M')}[/cyan]"
|
|
303
|
-
)
|
|
304
|
-
watch_branch.add(
|
|
305
|
-
f"Files synced: [green]{info.system.watch_status.get('synced_files', 0)}[/green]"
|
|
306
|
-
)
|
|
307
|
-
watch_branch.add(
|
|
308
|
-
f"Errors: [{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]{info.system.watch_status.get('error_count', 0)}[/{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]"
|
|
309
|
-
)
|
|
310
|
-
else:
|
|
311
|
-
system_tree.add("[yellow]Watch service not running[/yellow]")
|
|
312
|
-
|
|
313
|
-
console.print(system_tree)
|
|
314
|
-
|
|
315
317
|
# Available projects
|
|
316
318
|
projects_table = Table(title="📁 Available Projects")
|
|
317
319
|
projects_table.add_column("Name", style="blue")
|
|
@@ -2,19 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from typing import Set, Dict
|
|
5
|
+
from typing import Annotated, Optional
|
|
5
6
|
|
|
7
|
+
from mcp.server.fastmcp.exceptions import ToolError
|
|
6
8
|
import typer
|
|
7
9
|
from loguru import logger
|
|
8
10
|
from rich.console import Console
|
|
9
11
|
from rich.panel import Panel
|
|
10
12
|
from rich.tree import Tree
|
|
11
13
|
|
|
12
|
-
from basic_memory import db
|
|
13
14
|
from basic_memory.cli.app import app
|
|
14
|
-
from basic_memory.
|
|
15
|
-
from basic_memory.
|
|
16
|
-
from basic_memory.
|
|
17
|
-
from basic_memory.
|
|
15
|
+
from basic_memory.mcp.async_client import get_client
|
|
16
|
+
from basic_memory.mcp.tools.utils import call_post
|
|
17
|
+
from basic_memory.schemas import SyncReportResponse
|
|
18
|
+
from basic_memory.mcp.project_context import get_active_project
|
|
18
19
|
|
|
19
20
|
# Create rich console
|
|
20
21
|
console = Console()
|
|
@@ -47,7 +48,7 @@ def add_files_to_tree(
|
|
|
47
48
|
branch.add(f"[{style}]{file_name}[/{style}]")
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
def group_changes_by_directory(changes:
|
|
51
|
+
def group_changes_by_directory(changes: SyncReportResponse) -> Dict[str, Dict[str, int]]:
|
|
51
52
|
"""Group changes by directory for summary view."""
|
|
52
53
|
by_dir = {}
|
|
53
54
|
for change_type, paths in [
|
|
@@ -87,7 +88,9 @@ def build_directory_summary(counts: Dict[str, int]) -> str:
|
|
|
87
88
|
return " ".join(parts)
|
|
88
89
|
|
|
89
90
|
|
|
90
|
-
def display_changes(
|
|
91
|
+
def display_changes(
|
|
92
|
+
project_name: str, title: str, changes: SyncReportResponse, verbose: bool = False
|
|
93
|
+
):
|
|
91
94
|
"""Display changes using Rich for better visualization."""
|
|
92
95
|
tree = Tree(f"{project_name}: {title}")
|
|
93
96
|
|
|
@@ -122,33 +125,33 @@ def display_changes(project_name: str, title: str, changes: SyncReport, verbose:
|
|
|
122
125
|
console.print(Panel(tree, expand=False))
|
|
123
126
|
|
|
124
127
|
|
|
125
|
-
async def run_status(verbose: bool = False): # pragma: no cover
|
|
128
|
+
async def run_status(project: Optional[str] = None, verbose: bool = False): # pragma: no cover
|
|
126
129
|
"""Check sync status of files vs database."""
|
|
127
|
-
# Check knowledge/ directory
|
|
128
130
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
try:
|
|
132
|
+
async with get_client() as client:
|
|
133
|
+
project_item = await get_active_project(client, project, None)
|
|
134
|
+
response = await call_post(client, f"{project_item.project_url}/project/status")
|
|
135
|
+
sync_report = SyncReportResponse.model_validate(response.json())
|
|
131
136
|
|
|
132
|
-
|
|
133
|
-
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
|
134
|
-
)
|
|
135
|
-
project_repository = ProjectRepository(session_maker)
|
|
136
|
-
project = await project_repository.get_by_name(config.project)
|
|
137
|
-
if not project: # pragma: no cover
|
|
138
|
-
raise Exception(f"Project '{config.project}' not found")
|
|
137
|
+
display_changes(project_item.name, "Status", sync_report, verbose)
|
|
139
138
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
except (ValueError, ToolError) as e:
|
|
140
|
+
console.print(f"[red]✗ Error: {e}[/red]")
|
|
141
|
+
raise typer.Exit(1)
|
|
143
142
|
|
|
144
143
|
|
|
145
144
|
@app.command()
|
|
146
145
|
def status(
|
|
146
|
+
project: Annotated[
|
|
147
|
+
Optional[str],
|
|
148
|
+
typer.Option(help="The project name."),
|
|
149
|
+
] = None,
|
|
147
150
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed file information"),
|
|
148
151
|
):
|
|
149
152
|
"""Show sync status between files and database."""
|
|
150
153
|
try:
|
|
151
|
-
asyncio.run(run_status(verbose)) # pragma: no cover
|
|
154
|
+
asyncio.run(run_status(project, verbose)) # pragma: no cover
|
|
152
155
|
except Exception as e:
|
|
153
156
|
logger.error(f"Error checking status: {e}")
|
|
154
157
|
typer.echo(f"Error checking status: {e}", err=True)
|