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.

Files changed (84) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +5 -9
  3. basic_memory/api/app.py +10 -4
  4. basic_memory/api/routers/directory_router.py +23 -2
  5. basic_memory/api/routers/knowledge_router.py +25 -8
  6. basic_memory/api/routers/project_router.py +100 -4
  7. basic_memory/cli/app.py +9 -28
  8. basic_memory/cli/auth.py +277 -0
  9. basic_memory/cli/commands/cloud/__init__.py +5 -0
  10. basic_memory/cli/commands/cloud/api_client.py +112 -0
  11. basic_memory/cli/commands/cloud/bisync_commands.py +818 -0
  12. basic_memory/cli/commands/cloud/core_commands.py +288 -0
  13. basic_memory/cli/commands/cloud/mount_commands.py +295 -0
  14. basic_memory/cli/commands/cloud/rclone_config.py +288 -0
  15. basic_memory/cli/commands/cloud/rclone_installer.py +198 -0
  16. basic_memory/cli/commands/command_utils.py +43 -0
  17. basic_memory/cli/commands/import_memory_json.py +0 -4
  18. basic_memory/cli/commands/mcp.py +77 -60
  19. basic_memory/cli/commands/project.py +154 -152
  20. basic_memory/cli/commands/status.py +25 -22
  21. basic_memory/cli/commands/sync.py +45 -228
  22. basic_memory/cli/commands/tool.py +87 -16
  23. basic_memory/cli/main.py +1 -0
  24. basic_memory/config.py +131 -21
  25. basic_memory/db.py +104 -3
  26. basic_memory/deps.py +27 -8
  27. basic_memory/file_utils.py +37 -13
  28. basic_memory/ignore_utils.py +295 -0
  29. basic_memory/markdown/plugins.py +9 -7
  30. basic_memory/mcp/async_client.py +124 -14
  31. basic_memory/mcp/project_context.py +141 -0
  32. basic_memory/mcp/prompts/ai_assistant_guide.py +49 -4
  33. basic_memory/mcp/prompts/continue_conversation.py +17 -16
  34. basic_memory/mcp/prompts/recent_activity.py +116 -32
  35. basic_memory/mcp/prompts/search.py +13 -12
  36. basic_memory/mcp/prompts/utils.py +11 -4
  37. basic_memory/mcp/resources/ai_assistant_guide.md +211 -341
  38. basic_memory/mcp/resources/project_info.py +27 -11
  39. basic_memory/mcp/server.py +0 -37
  40. basic_memory/mcp/tools/__init__.py +5 -6
  41. basic_memory/mcp/tools/build_context.py +67 -56
  42. basic_memory/mcp/tools/canvas.py +38 -26
  43. basic_memory/mcp/tools/chatgpt_tools.py +187 -0
  44. basic_memory/mcp/tools/delete_note.py +81 -47
  45. basic_memory/mcp/tools/edit_note.py +155 -138
  46. basic_memory/mcp/tools/list_directory.py +112 -99
  47. basic_memory/mcp/tools/move_note.py +181 -101
  48. basic_memory/mcp/tools/project_management.py +113 -277
  49. basic_memory/mcp/tools/read_content.py +91 -74
  50. basic_memory/mcp/tools/read_note.py +152 -115
  51. basic_memory/mcp/tools/recent_activity.py +471 -68
  52. basic_memory/mcp/tools/search.py +105 -92
  53. basic_memory/mcp/tools/sync_status.py +136 -130
  54. basic_memory/mcp/tools/utils.py +4 -0
  55. basic_memory/mcp/tools/view_note.py +44 -33
  56. basic_memory/mcp/tools/write_note.py +151 -90
  57. basic_memory/models/knowledge.py +12 -6
  58. basic_memory/models/project.py +6 -2
  59. basic_memory/repository/entity_repository.py +89 -82
  60. basic_memory/repository/relation_repository.py +13 -0
  61. basic_memory/repository/repository.py +18 -5
  62. basic_memory/repository/search_repository.py +46 -2
  63. basic_memory/schemas/__init__.py +6 -0
  64. basic_memory/schemas/base.py +39 -11
  65. basic_memory/schemas/cloud.py +46 -0
  66. basic_memory/schemas/memory.py +90 -21
  67. basic_memory/schemas/project_info.py +9 -10
  68. basic_memory/schemas/sync_report.py +48 -0
  69. basic_memory/services/context_service.py +25 -11
  70. basic_memory/services/directory_service.py +124 -3
  71. basic_memory/services/entity_service.py +100 -48
  72. basic_memory/services/initialization.py +30 -11
  73. basic_memory/services/project_service.py +101 -24
  74. basic_memory/services/search_service.py +16 -8
  75. basic_memory/sync/sync_service.py +173 -34
  76. basic_memory/sync/watch_service.py +101 -40
  77. basic_memory/utils.py +14 -4
  78. {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/METADATA +57 -9
  79. basic_memory-0.15.1.dist-info/RECORD +146 -0
  80. basic_memory/mcp/project_session.py +0 -120
  81. basic_memory-0.14.4.dist-info/RECORD +0 -133
  82. {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/WHEEL +0 -0
  83. {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/entry_points.txt +0 -0
  84. {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -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
- @app.command()
21
- def mcp(
22
- transport: str = typer.Option("stdio", help="Transport type: stdio, streamable-http, or sse"),
23
- host: str = typer.Option(
24
- "0.0.0.0", help="Host for HTTP transports (use 0.0.0.0 to allow external connections)"
25
- ),
26
- port: int = typer.Option(8000, help="Port for HTTP transports"),
27
- path: str = typer.Option("/mcp", help="Path prefix for streamable-http transport"),
28
- ): # pragma: no cover
29
- """Run the MCP server with configurable transport options.
30
-
31
- This command starts an MCP server using one of three transport options:
32
-
33
- - stdio: Standard I/O (good for local usage)
34
- - streamable-http: Recommended for web deployments (default)
35
- - sse: Server-Sent Events (for compatibility with existing clients)
36
- """
37
-
38
- from basic_memory.services.initialization import initialize_file_sync
39
-
40
- # Use unified thread-based sync approach for both transports
41
- import threading
42
-
43
- app_config = ConfigManager().config
44
-
45
- def run_file_sync():
46
- """Run file sync in a separate thread with its own event loop."""
47
- loop = asyncio.new_event_loop()
48
- asyncio.set_event_loop(loop)
49
- try:
50
- loop.run_until_complete(initialize_file_sync(app_config))
51
- except Exception as e:
52
- logger.error(f"File sync error: {e}", err=True)
53
- finally:
54
- loop.close()
55
-
56
- logger.info(f"Sync changes enabled: {app_config.sync_changes}")
57
- if app_config.sync_changes:
58
- # Start the sync thread
59
- sync_thread = threading.Thread(target=run_file_sync, daemon=True)
60
- sync_thread.start()
61
- logger.info("Started file sync in background")
62
-
63
- # Now run the MCP server (blocks)
64
- logger.info(f"Starting MCP server with {transport.upper()} transport")
65
-
66
- if transport == "stdio":
67
- mcp_server.run(
68
- transport=transport,
69
- )
70
- elif transport == "streamable-http" or transport == "sse":
71
- mcp_server.run(
72
- transport=transport,
73
- host=host,
74
- port=port,
75
- path=path,
76
- log_level="INFO",
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.mcp.project_session import session
13
- from basic_memory.mcp.resources.project_info import project_info
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 rich.tree import Tree
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 configured projects."""
47
- # Use API to list projects
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
- response = asyncio.run(call_get(client, "/projects/projects"))
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="yellow")
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
- is_active = "✓" if session.get_current_project() == project.name else ""
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
- @project_app.command("add")
70
- def add_project(
71
- name: str = typer.Argument(..., help="Name of the project"),
72
- path: str = typer.Argument(..., help="Path to the project directory"),
73
- set_default: bool = typer.Option(False, "--default", help="Set as default project"),
74
- ) -> None:
75
- """Add a new project."""
76
- # Resolve to absolute path
77
- resolved_path = Path(os.path.abspath(os.path.expanduser(path))).as_posix()
78
-
79
- try:
80
- data = {"name": name, "path": resolved_path, "set_default": set_default}
81
-
82
- response = asyncio.run(call_post(client, "/projects/projects", json=data))
83
- result = ProjectStatusResponse.model_validate(response.json())
84
-
85
- console.print(f"[green]{result.message}[/green]")
86
- except Exception as e:
87
- console.print(f"[red]Error adding project: {str(e)}[/red]")
88
- raise typer.Exit(1)
89
-
90
- # Display usage hint
91
- console.print("\nTo use this project:")
92
- console.print(f" basic-memory --project={name} <command>")
93
- console.print(" # or")
94
- console.print(f" basic-memory project default {name}")
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 from configuration."""
102
- try:
103
- project_permalink = generate_permalink(name)
104
- response = asyncio.run(call_delete(client, f"/projects/{project_permalink}"))
105
- result = ProjectStatusResponse.model_validate(response.json())
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
- @project_app.command("default")
117
- def set_default_project(
118
- name: str = typer.Argument(..., help="Name of the project to set as default"),
119
- ) -> None:
120
- """Set the default project and activate it for the current session."""
121
- try:
122
- project_permalink = generate_permalink(name)
123
- response = asyncio.run(call_put(client, f"/projects/{project_permalink}/default"))
124
- result = ProjectStatusResponse.model_validate(response.json())
125
-
126
- console.print(f"[green]{result.message}[/green]")
127
- except Exception as e:
128
- console.print(f"[red]Error setting default project: {str(e)}[/red]")
129
- raise typer.Exit(1)
130
-
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
133
- console.print("[green]Project activated for current session[/green]")
134
-
135
-
136
- @project_app.command("sync-config")
137
- def synchronize_projects() -> None:
138
- """Synchronize project config between configuration file and database."""
139
- # Call the API to synchronize projects
140
-
141
- try:
142
- response = asyncio.run(call_post(client, "/projects/sync"))
143
- result = ProjectStatusResponse.model_validate(response.json())
144
-
145
- console.print(f"[green]{result.message}[/green]")
146
- except Exception as e: # pragma: no cover
147
- console.print(f"[red]Error synchronizing projects: {str(e)}[/red]")
148
- raise typer.Exit(1)
149
-
150
-
151
- @project_app.command("move")
152
- def move_project(
153
- name: str = typer.Argument(..., help="Name of the project to move"),
154
- new_path: str = typer.Argument(..., help="New absolute path for the project"),
155
- ) -> None:
156
- """Move a project to a new location."""
157
- # Resolve to absolute path
158
- resolved_path = Path(os.path.abspath(os.path.expanduser(new_path))).as_posix()
159
-
160
- try:
161
- data = {"path": resolved_path}
162
-
163
- project_permalink = generate_permalink(name)
164
- current_project = session.get_current_project()
165
- response = asyncio.run(
166
- call_patch(client, f"/{current_project}/project/{project_permalink}", json=data)
167
- )
168
- result = ProjectStatusResponse.model_validate(response.json())
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
- console.print(f"[green]{result.message}[/green]")
207
+ try:
208
+ result = asyncio.run(_move_project())
209
+ console.print(f"[green]{result.message}[/green]")
171
210
 
172
- # Show important file movement reminder
173
- console.print() # Empty line for spacing
174
- console.print(
175
- Panel(
176
- "[bold red]IMPORTANT:[/bold red] Project configuration updated successfully.\n\n"
177
- "[yellow]You must manually move your project files from the old location to:[/yellow]\n"
178
- f"[cyan]{resolved_path}[/cyan]\n\n"
179
- "[dim]Basic Memory has only updated the configuration - your files remain in their original location.[/dim]",
180
- title="⚠️ Manual File Movement Required",
181
- border_style="yellow",
182
- expand=False,
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
- except Exception as e:
187
- console.print(f"[red]Error moving project: {str(e)}[/red]")
188
- raise typer.Exit(1)
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(project_info.fn()) # type: ignore # pyright: ignore [reportAttributeAccessIssue]
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.cli.commands.sync import get_sync_service
15
- from basic_memory.config import ConfigManager, get_project_config
16
- from basic_memory.repository import ProjectRepository
17
- from basic_memory.sync.sync_service import SyncReport
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: SyncReport) -> Dict[str, Dict[str, int]]:
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(project_name: str, title: str, changes: SyncReport, verbose: bool = False):
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
- app_config = ConfigManager().config
130
- config = get_project_config()
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
- _, session_maker = await db.get_or_create_db(
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
- sync_service = await get_sync_service(project)
141
- knowledge_changes = await sync_service.scan(config.home)
142
- display_changes(project.name, "Status", knowledge_changes, verbose)
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)