basic-memory 0.12.3__py3-none-any.whl → 0.13.0b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of basic-memory might be problematic. Click here for more details.

Files changed (107) hide show
  1. basic_memory/__init__.py +7 -1
  2. basic_memory/alembic/env.py +1 -1
  3. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  4. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -5
  5. basic_memory/api/app.py +43 -13
  6. basic_memory/api/routers/__init__.py +4 -2
  7. basic_memory/api/routers/directory_router.py +63 -0
  8. basic_memory/api/routers/importer_router.py +152 -0
  9. basic_memory/api/routers/knowledge_router.py +127 -38
  10. basic_memory/api/routers/management_router.py +78 -0
  11. basic_memory/api/routers/memory_router.py +4 -59
  12. basic_memory/api/routers/project_router.py +230 -0
  13. basic_memory/api/routers/prompt_router.py +260 -0
  14. basic_memory/api/routers/search_router.py +3 -21
  15. basic_memory/api/routers/utils.py +130 -0
  16. basic_memory/api/template_loader.py +292 -0
  17. basic_memory/cli/app.py +20 -21
  18. basic_memory/cli/commands/__init__.py +2 -1
  19. basic_memory/cli/commands/auth.py +136 -0
  20. basic_memory/cli/commands/db.py +3 -3
  21. basic_memory/cli/commands/import_chatgpt.py +31 -207
  22. basic_memory/cli/commands/import_claude_conversations.py +16 -142
  23. basic_memory/cli/commands/import_claude_projects.py +33 -143
  24. basic_memory/cli/commands/import_memory_json.py +26 -83
  25. basic_memory/cli/commands/mcp.py +71 -18
  26. basic_memory/cli/commands/project.py +99 -67
  27. basic_memory/cli/commands/status.py +19 -9
  28. basic_memory/cli/commands/sync.py +44 -58
  29. basic_memory/cli/main.py +1 -5
  30. basic_memory/config.py +144 -88
  31. basic_memory/db.py +6 -4
  32. basic_memory/deps.py +227 -30
  33. basic_memory/importers/__init__.py +27 -0
  34. basic_memory/importers/base.py +79 -0
  35. basic_memory/importers/chatgpt_importer.py +222 -0
  36. basic_memory/importers/claude_conversations_importer.py +172 -0
  37. basic_memory/importers/claude_projects_importer.py +148 -0
  38. basic_memory/importers/memory_json_importer.py +93 -0
  39. basic_memory/importers/utils.py +58 -0
  40. basic_memory/markdown/entity_parser.py +5 -2
  41. basic_memory/mcp/auth_provider.py +270 -0
  42. basic_memory/mcp/external_auth_provider.py +321 -0
  43. basic_memory/mcp/project_session.py +103 -0
  44. basic_memory/mcp/prompts/continue_conversation.py +18 -68
  45. basic_memory/mcp/prompts/recent_activity.py +19 -3
  46. basic_memory/mcp/prompts/search.py +14 -140
  47. basic_memory/mcp/prompts/utils.py +3 -3
  48. basic_memory/mcp/{tools → resources}/project_info.py +6 -2
  49. basic_memory/mcp/server.py +82 -8
  50. basic_memory/mcp/supabase_auth_provider.py +463 -0
  51. basic_memory/mcp/tools/__init__.py +20 -0
  52. basic_memory/mcp/tools/build_context.py +11 -1
  53. basic_memory/mcp/tools/canvas.py +15 -2
  54. basic_memory/mcp/tools/delete_note.py +12 -4
  55. basic_memory/mcp/tools/edit_note.py +297 -0
  56. basic_memory/mcp/tools/list_directory.py +154 -0
  57. basic_memory/mcp/tools/move_note.py +87 -0
  58. basic_memory/mcp/tools/project_management.py +300 -0
  59. basic_memory/mcp/tools/read_content.py +15 -6
  60. basic_memory/mcp/tools/read_note.py +17 -5
  61. basic_memory/mcp/tools/recent_activity.py +11 -2
  62. basic_memory/mcp/tools/search.py +10 -1
  63. basic_memory/mcp/tools/utils.py +137 -12
  64. basic_memory/mcp/tools/write_note.py +11 -15
  65. basic_memory/models/__init__.py +3 -2
  66. basic_memory/models/knowledge.py +16 -4
  67. basic_memory/models/project.py +80 -0
  68. basic_memory/models/search.py +8 -5
  69. basic_memory/repository/__init__.py +2 -0
  70. basic_memory/repository/entity_repository.py +8 -3
  71. basic_memory/repository/observation_repository.py +35 -3
  72. basic_memory/repository/project_info_repository.py +3 -2
  73. basic_memory/repository/project_repository.py +85 -0
  74. basic_memory/repository/relation_repository.py +8 -2
  75. basic_memory/repository/repository.py +107 -15
  76. basic_memory/repository/search_repository.py +87 -27
  77. basic_memory/schemas/__init__.py +6 -0
  78. basic_memory/schemas/directory.py +30 -0
  79. basic_memory/schemas/importer.py +34 -0
  80. basic_memory/schemas/memory.py +26 -12
  81. basic_memory/schemas/project_info.py +112 -2
  82. basic_memory/schemas/prompt.py +90 -0
  83. basic_memory/schemas/request.py +56 -2
  84. basic_memory/schemas/search.py +1 -1
  85. basic_memory/services/__init__.py +2 -1
  86. basic_memory/services/context_service.py +208 -95
  87. basic_memory/services/directory_service.py +167 -0
  88. basic_memory/services/entity_service.py +385 -5
  89. basic_memory/services/exceptions.py +6 -0
  90. basic_memory/services/file_service.py +14 -15
  91. basic_memory/services/initialization.py +144 -67
  92. basic_memory/services/link_resolver.py +16 -8
  93. basic_memory/services/project_service.py +548 -0
  94. basic_memory/services/search_service.py +77 -2
  95. basic_memory/sync/background_sync.py +25 -0
  96. basic_memory/sync/sync_service.py +10 -9
  97. basic_memory/sync/watch_service.py +63 -39
  98. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  99. basic_memory/templates/prompts/search.hbs +101 -0
  100. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/METADATA +23 -1
  101. basic_memory-0.13.0b2.dist-info/RECORD +132 -0
  102. basic_memory/api/routers/project_info_router.py +0 -274
  103. basic_memory/mcp/main.py +0 -24
  104. basic_memory-0.12.3.dist-info/RECORD +0 -100
  105. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/WHEEL +0 -0
  106. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/entry_points.txt +0 -0
  107. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/licenses/LICENSE +0 -0
@@ -3,94 +3,20 @@
3
3
  import asyncio
4
4
  import json
5
5
  from pathlib import Path
6
- from typing import Dict, Any, List, Annotated
6
+ from typing import Annotated
7
7
 
8
8
  import typer
9
- from loguru import logger
10
- from rich.console import Console
11
- from rich.panel import Panel
12
- from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
13
-
14
9
  from basic_memory.cli.app import import_app
15
10
  from basic_memory.config import config
11
+ from basic_memory.importers.memory_json_importer import MemoryJsonImporter
16
12
  from basic_memory.markdown import EntityParser, MarkdownProcessor
17
- from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter, Observation, Relation
13
+ from loguru import logger
14
+ from rich.console import Console
15
+ from rich.panel import Panel
18
16
 
19
17
  console = Console()
20
18
 
21
19
 
22
- async def process_memory_json(
23
- json_path: Path, base_path: Path, markdown_processor: MarkdownProcessor
24
- ):
25
- """Import entities from memory.json using markdown processor."""
26
-
27
- # First pass - collect all relations by source entity
28
- entity_relations: Dict[str, List[Relation]] = {}
29
- entities: Dict[str, Dict[str, Any]] = {}
30
-
31
- with Progress(
32
- SpinnerColumn(),
33
- TextColumn("[progress.description]{task.description}"),
34
- BarColumn(),
35
- TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
36
- console=console,
37
- ) as progress:
38
- read_task = progress.add_task("Reading memory.json...", total=None)
39
-
40
- # First pass - collect entities and relations
41
- with open(json_path, encoding="utf-8") as f:
42
- lines = f.readlines()
43
- progress.update(read_task, total=len(lines))
44
-
45
- for line in lines:
46
- data = json.loads(line)
47
- if data["type"] == "entity":
48
- entities[data["name"]] = data
49
- elif data["type"] == "relation":
50
- # Store relation with its source entity
51
- source = data.get("from") or data.get("from_id")
52
- if source not in entity_relations:
53
- entity_relations[source] = []
54
- entity_relations[source].append(
55
- Relation(
56
- type=data.get("relationType") or data.get("relation_type"),
57
- target=data.get("to") or data.get("to_id"),
58
- )
59
- )
60
- progress.update(read_task, advance=1)
61
-
62
- # Second pass - create and write entities
63
- write_task = progress.add_task("Creating entities...", total=len(entities))
64
-
65
- entities_created = 0
66
- for name, entity_data in entities.items():
67
- entity = EntityMarkdown(
68
- frontmatter=EntityFrontmatter(
69
- metadata={
70
- "type": entity_data["entityType"],
71
- "title": name,
72
- "permalink": f"{entity_data['entityType']}/{name}",
73
- }
74
- ),
75
- content=f"# {name}\n",
76
- observations=[Observation(content=obs) for obs in entity_data["observations"]],
77
- relations=entity_relations.get(
78
- name, []
79
- ), # Add any relations where this entity is the source
80
- )
81
-
82
- # Let markdown processor handle writing
83
- file_path = base_path / f"{entity_data['entityType']}/{name}.md"
84
- await markdown_processor.write_file(file_path, entity)
85
- entities_created += 1
86
- progress.update(write_task, advance=1)
87
-
88
- return {
89
- "entities": entities_created,
90
- "relations": sum(len(rels) for rels in entity_relations.values()),
91
- }
92
-
93
-
94
20
  async def get_markdown_processor() -> MarkdownProcessor:
95
21
  """Get MarkdownProcessor instance."""
96
22
  entity_parser = EntityParser(config.home)
@@ -102,6 +28,9 @@ def memory_json(
102
28
  json_path: Annotated[Path, typer.Argument(..., help="Path to memory.json file")] = Path(
103
29
  "memory.json"
104
30
  ),
31
+ destination_folder: Annotated[
32
+ str, typer.Option(help="Optional destination folder within the project")
33
+ ] = "",
105
34
  ):
106
35
  """Import entities and relations from a memory.json file.
107
36
 
@@ -121,17 +50,31 @@ def memory_json(
121
50
  # Get markdown processor
122
51
  markdown_processor = asyncio.run(get_markdown_processor())
123
52
 
53
+ # Create the importer
54
+ importer = MemoryJsonImporter(config.home, markdown_processor)
55
+
124
56
  # Process the file
125
- base_path = config.home
57
+ base_path = config.home if not destination_folder else config.home / destination_folder
126
58
  console.print(f"\nImporting from {json_path}...writing to {base_path}")
127
- results = asyncio.run(process_memory_json(json_path, base_path, markdown_processor))
59
+
60
+ # Run the import for json log format
61
+ file_data = []
62
+ with json_path.open("r", encoding="utf-8") as file:
63
+ for line in file:
64
+ json_data = json.loads(line)
65
+ file_data.append(json_data)
66
+ result = asyncio.run(importer.import_data(file_data, destination_folder))
67
+
68
+ if not result.success: # pragma: no cover
69
+ typer.echo(f"Error during import: {result.error_message}", err=True)
70
+ raise typer.Exit(1)
128
71
 
129
72
  # Show results
130
73
  console.print(
131
74
  Panel(
132
75
  f"[green]Import complete![/green]\n\n"
133
- f"Created {results['entities']} entities\n"
134
- f"Added {results['relations']} relations",
76
+ f"Created {result.entities} entities\n"
77
+ f"Added {result.relations} relations",
135
78
  expand=False,
136
79
  )
137
80
  )
@@ -1,6 +1,8 @@
1
- """MCP server command."""
1
+ """MCP server command with streamable HTTP transport."""
2
+
3
+ import asyncio
4
+ import typer
2
5
 
3
- import basic_memory
4
6
  from basic_memory.cli.app import app
5
7
 
6
8
  # Import mcp instance
@@ -9,27 +11,78 @@ from basic_memory.mcp.server import mcp as mcp_server # pragma: no cover
9
11
  # Import mcp tools to register them
10
12
  import basic_memory.mcp.tools # noqa: F401 # pragma: no cover
11
13
 
14
+ # Import prompts to register them
15
+ import basic_memory.mcp.prompts # noqa: F401 # pragma: no cover
16
+ from loguru import logger
17
+
12
18
 
13
19
  @app.command()
14
- def mcp(): # pragma: no cover
15
- """Run the MCP server"""
16
- from basic_memory.config import config
17
- import asyncio
18
- from basic_memory.services.initialization import initialize_database
20
+ def mcp(
21
+ transport: str = typer.Option("stdio", help="Transport type: stdio, streamable-http, or sse"),
22
+ host: str = typer.Option(
23
+ "0.0.0.0", help="Host for HTTP transports (use 0.0.0.0 to allow external connections)"
24
+ ),
25
+ port: int = typer.Option(8000, help="Port for HTTP transports"),
26
+ path: str = typer.Option("/mcp", help="Path prefix for streamable-http transport"),
27
+ ): # pragma: no cover
28
+ """Run the MCP server with configurable transport options.
29
+
30
+ This command starts an MCP server using one of three transport options:
31
+
32
+ - stdio: Standard I/O (good for local usage)
33
+ - streamable-http: Recommended for web deployments (default)
34
+ - sse: Server-Sent Events (for compatibility with existing clients)
35
+ """
36
+
37
+ # Check if OAuth is enabled
38
+ import os
39
+
40
+ auth_enabled = os.getenv("FASTMCP_AUTH_ENABLED", "false").lower() == "true"
41
+ if auth_enabled:
42
+ logger.info("OAuth authentication is ENABLED")
43
+ logger.info(f"Issuer URL: {os.getenv('FASTMCP_AUTH_ISSUER_URL', 'http://localhost:8000')}")
44
+ if os.getenv("FASTMCP_AUTH_REQUIRED_SCOPES"):
45
+ logger.info(f"Required scopes: {os.getenv('FASTMCP_AUTH_REQUIRED_SCOPES')}")
46
+ else:
47
+ logger.info("OAuth authentication is DISABLED")
48
+
49
+ from basic_memory.config import app_config
50
+ from basic_memory.services.initialization import initialize_file_sync
19
51
 
20
- # First, run just the database migrations synchronously
21
- asyncio.run(initialize_database(config))
52
+ # Start the MCP server with the specified transport
22
53
 
23
- # Load config to check if sync is enabled
24
- from basic_memory.config import config_manager
54
+ # Use unified thread-based sync approach for both transports
55
+ import threading
25
56
 
26
- basic_memory_config = config_manager.load_config()
57
+ def run_file_sync():
58
+ """Run file sync in a separate thread with its own event loop."""
59
+ loop = asyncio.new_event_loop()
60
+ asyncio.set_event_loop(loop)
61
+ try:
62
+ loop.run_until_complete(initialize_file_sync(app_config))
63
+ except Exception as e:
64
+ logger.error(f"File sync error: {e}", err=True)
65
+ finally:
66
+ loop.close()
27
67
 
28
- if basic_memory_config.sync_changes:
29
- # For now, we'll just log that sync will be handled by the MCP server
30
- from loguru import logger
68
+ logger.info(f"Sync changes enabled: {app_config.sync_changes}")
69
+ if app_config.sync_changes:
70
+ # Start the sync thread
71
+ sync_thread = threading.Thread(target=run_file_sync, daemon=True)
72
+ sync_thread.start()
73
+ logger.info("Started file sync in background")
31
74
 
32
- logger.info("File sync will be handled by the MCP server")
75
+ # Now run the MCP server (blocks)
76
+ logger.info(f"Starting MCP server with {transport.upper()} transport")
33
77
 
34
- # Start the MCP server
35
- mcp_server.run()
78
+ if transport == "stdio":
79
+ mcp_server.run(
80
+ transport=transport,
81
+ )
82
+ elif transport == "streamable-http" or transport == "sse":
83
+ mcp_server.run(
84
+ transport=transport,
85
+ host=host,
86
+ port=port,
87
+ path=path,
88
+ )
@@ -9,13 +9,21 @@ 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.config import ConfigManager, config
13
- from basic_memory.mcp.tools.project_info import project_info
12
+ from basic_memory.config import config
13
+ from basic_memory.mcp.project_session import session
14
+ from basic_memory.mcp.resources.project_info import project_info
14
15
  import json
15
16
  from datetime import datetime
16
17
 
17
18
  from rich.panel import Panel
18
19
  from rich.tree import Tree
20
+ from basic_memory.mcp.async_client import client
21
+ from basic_memory.mcp.tools.utils import call_get
22
+ from basic_memory.schemas.project_info import ProjectList
23
+ from basic_memory.mcp.tools.utils import call_post
24
+ from basic_memory.schemas.project_info import ProjectStatusResponse
25
+ from basic_memory.mcp.tools.utils import call_delete
26
+ from basic_memory.mcp.tools.utils import call_put
19
27
 
20
28
  console = Console()
21
29
 
@@ -28,112 +36,135 @@ def format_path(path: str) -> str:
28
36
  """Format a path for display, using ~ for home directory."""
29
37
  home = str(Path.home())
30
38
  if path.startswith(home):
31
- return path.replace(home, "~", 1)
39
+ return path.replace(home, "~", 1) # pragma: no cover
32
40
  return path
33
41
 
34
42
 
35
43
  @project_app.command("list")
36
44
  def list_projects() -> None:
37
45
  """List all configured projects."""
38
- config_manager = ConfigManager()
39
- projects = config_manager.projects
46
+ # Use API to list projects
40
47
 
41
- table = Table(title="Basic Memory Projects")
42
- table.add_column("Name", style="cyan")
43
- table.add_column("Path", style="green")
44
- table.add_column("Default", style="yellow")
45
- table.add_column("Active", style="magenta")
48
+ project_url = config.project_url
46
49
 
47
- default_project = config_manager.default_project
48
- active_project = config.project
49
-
50
- for name, path in projects.items():
51
- is_default = "✓" if name == default_project else ""
52
- is_active = "" if name == active_project else ""
53
- table.add_row(name, format_path(path), is_default, is_active)
54
-
55
- console.print(table)
50
+ try:
51
+ response = asyncio.run(call_get(client, f"{project_url}/project/projects"))
52
+ result = ProjectList.model_validate(response.json())
53
+
54
+ table = Table(title="Basic Memory Projects")
55
+ table.add_column("Name", style="cyan")
56
+ table.add_column("Path", style="green")
57
+ table.add_column("Default", style="yellow")
58
+ table.add_column("Active", style="magenta")
59
+
60
+ for project in result.projects:
61
+ is_default = "✓" if project.is_default else ""
62
+ is_active = "✓" if session.get_current_project() == project.name else ""
63
+ table.add_row(project.name, format_path(project.path), is_default, is_active)
64
+
65
+ console.print(table)
66
+ except Exception as e:
67
+ console.print(f"[red]Error listing projects: {str(e)}[/red]")
68
+ console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]")
69
+ raise typer.Exit(1)
56
70
 
57
71
 
58
72
  @project_app.command("add")
59
73
  def add_project(
60
74
  name: str = typer.Argument(..., help="Name of the project"),
61
75
  path: str = typer.Argument(..., help="Path to the project directory"),
76
+ set_default: bool = typer.Option(False, "--default", help="Set as default project"),
62
77
  ) -> None:
63
78
  """Add a new project."""
64
- config_manager = ConfigManager()
79
+ # Resolve to absolute path
80
+ resolved_path = os.path.abspath(os.path.expanduser(path))
65
81
 
66
82
  try:
67
- # Resolve to absolute path
68
- resolved_path = os.path.abspath(os.path.expanduser(path))
69
- config_manager.add_project(name, resolved_path)
70
- console.print(f"[green]Project '{name}' added at {format_path(resolved_path)}[/green]")
71
-
72
- # Display usage hint
73
- console.print("\nTo use this project:")
74
- console.print(f" basic-memory --project={name} <command>")
75
- console.print(" # or")
76
- console.print(f" basic-memory project default {name}")
77
- except ValueError as e:
78
- console.print(f"[red]Error: {e}[/red]")
83
+ project_url = config.project_url
84
+ data = {"name": name, "path": resolved_path, "set_default": set_default}
85
+
86
+ response = asyncio.run(call_post(client, f"{project_url}/project/projects", json=data))
87
+ result = ProjectStatusResponse.model_validate(response.json())
88
+
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
+ console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]")
79
93
  raise typer.Exit(1)
80
94
 
95
+ # Display usage hint
96
+ console.print("\nTo use this project:")
97
+ console.print(f" basic-memory --project={name} <command>")
98
+ console.print(" # or")
99
+ console.print(f" basic-memory project default {name}")
100
+
81
101
 
82
102
  @project_app.command("remove")
83
103
  def remove_project(
84
104
  name: str = typer.Argument(..., help="Name of the project to remove"),
85
105
  ) -> None:
86
106
  """Remove a project from configuration."""
87
- config_manager = ConfigManager()
88
-
89
107
  try:
90
- config_manager.remove_project(name)
91
- console.print(f"[green]Project '{name}' removed from configuration[/green]")
92
- console.print("[yellow]Note: The project files have not been deleted from disk.[/yellow]")
93
- except ValueError as e: # pragma: no cover
94
- console.print(f"[red]Error: {e}[/red]")
108
+ project_url = config.project_url
109
+
110
+ response = asyncio.run(call_delete(client, f"{project_url}/project/projects/{name}"))
111
+ result = ProjectStatusResponse.model_validate(response.json())
112
+
113
+ console.print(f"[green]{result.message}[/green]")
114
+ except Exception as e:
115
+ console.print(f"[red]Error removing project: {str(e)}[/red]")
116
+ console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]")
95
117
  raise typer.Exit(1)
96
118
 
119
+ # Show this message regardless of method used
120
+ console.print("[yellow]Note: The project files have not been deleted from disk.[/yellow]")
121
+
97
122
 
98
123
  @project_app.command("default")
99
124
  def set_default_project(
100
125
  name: str = typer.Argument(..., help="Name of the project to set as default"),
101
126
  ) -> None:
102
127
  """Set the default project and activate it for the current session."""
103
- config_manager = ConfigManager()
104
-
105
128
  try:
106
- # Set the default project
107
- config_manager.set_default_project(name)
108
-
109
- # Also activate it for the current session by setting the environment variable
110
- os.environ["BASIC_MEMORY_PROJECT"] = name
129
+ project_url = config.project_url
111
130
 
112
- # Reload configuration to apply the change
113
- from importlib import reload
114
- from basic_memory import config as config_module
131
+ response = asyncio.run(call_put(client, f"{project_url}/project/projects/{name}/default"))
132
+ result = ProjectStatusResponse.model_validate(response.json())
115
133
 
116
- reload(config_module)
117
- console.print(f"[green]Project '{name}' set as default and activated[/green]")
118
- except ValueError as e: # pragma: no cover
119
- console.print(f"[red]Error: {e}[/red]")
134
+ console.print(f"[green]{result.message}[/green]")
135
+ except Exception as e:
136
+ console.print(f"[red]Error setting default project: {str(e)}[/red]")
137
+ console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]")
120
138
  raise typer.Exit(1)
121
139
 
140
+ # Always activate it for the current session
141
+ os.environ["BASIC_MEMORY_PROJECT"] = name
142
+
143
+ # Reload configuration to apply the change
144
+ from importlib import reload
145
+ from basic_memory import config as config_module
146
+
147
+ reload(config_module)
148
+
149
+ console.print("[green]Project activated for current session[/green]")
122
150
 
123
- @project_app.command("current")
124
- def show_current_project() -> None:
125
- """Show the current project."""
126
- config_manager = ConfigManager()
127
- current = os.environ.get("BASIC_MEMORY_PROJECT", config_manager.default_project)
151
+
152
+ @project_app.command("sync")
153
+ def synchronize_projects() -> None:
154
+ """Synchronize projects between configuration file and database."""
155
+ # Call the API to synchronize projects
156
+
157
+ project_url = config.project_url
128
158
 
129
159
  try:
130
- path = config_manager.get_project_path(current)
131
- console.print(f"Current project: [cyan]{current}[/cyan]")
132
- console.print(f"Path: [green]{format_path(str(path))}[/green]")
133
- console.print(f"Database: [blue]{format_path(str(config.database_path))}[/blue]")
134
- except ValueError: # pragma: no cover
135
- console.print(f"[yellow]Warning: Project '{current}' not found in configuration[/yellow]")
136
- console.print(f"Using default project: [cyan]{config_manager.default_project}[/cyan]")
160
+ response = asyncio.run(call_post(client, f"{project_url}/project/sync"))
161
+ result = ProjectStatusResponse.model_validate(response.json())
162
+
163
+ console.print(f"[green]{result.message}[/green]")
164
+ except Exception as e: # pragma: no cover
165
+ console.print(f"[red]Error synchronizing projects: {str(e)}[/red]")
166
+ console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]")
167
+ raise typer.Exit(1)
137
168
 
138
169
 
139
170
  @project_app.command("info")
@@ -266,9 +297,10 @@ def display_project_info(
266
297
  projects_table.add_column("Path", style="cyan")
267
298
  projects_table.add_column("Default", style="green")
268
299
 
269
- for name, path in info.available_projects.items():
300
+ for name, proj_info in info.available_projects.items():
270
301
  is_default = name == info.default_project
271
- projects_table.add_row(name, path, "" if is_default else "")
302
+ project_path = proj_info["path"]
303
+ projects_table.add_row(name, project_path, "✓" if is_default else "")
272
304
 
273
305
  console.print(projects_table)
274
306
 
@@ -9,10 +9,11 @@ from rich.console import Console
9
9
  from rich.panel import Panel
10
10
  from rich.tree import Tree
11
11
 
12
+ from basic_memory import db
12
13
  from basic_memory.cli.app import app
13
14
  from basic_memory.cli.commands.sync import get_sync_service
14
- from basic_memory.config import config
15
- from basic_memory.sync import SyncService
15
+ from basic_memory.config import config, app_config
16
+ from basic_memory.repository import ProjectRepository
16
17
  from basic_memory.sync.sync_service import SyncReport
17
18
 
18
19
  # Create rich console
@@ -86,9 +87,9 @@ def build_directory_summary(counts: Dict[str, int]) -> str:
86
87
  return " ".join(parts)
87
88
 
88
89
 
89
- def display_changes(title: str, changes: SyncReport, verbose: bool = False):
90
+ def display_changes(project_name: str, title: str, changes: SyncReport, verbose: bool = False):
90
91
  """Display changes using Rich for better visualization."""
91
- tree = Tree(title)
92
+ tree = Tree(f"{project_name}: {title}")
92
93
 
93
94
  if changes.total == 0:
94
95
  tree.add("No changes")
@@ -121,11 +122,21 @@ def display_changes(title: str, changes: SyncReport, verbose: bool = False):
121
122
  console.print(Panel(tree, expand=False))
122
123
 
123
124
 
124
- async def run_status(sync_service: SyncService, verbose: bool = False):
125
+ async def run_status(verbose: bool = False):
125
126
  """Check sync status of files vs database."""
126
127
  # Check knowledge/ directory
128
+
129
+ _, session_maker = await db.get_or_create_db(
130
+ db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
131
+ )
132
+ project_repository = ProjectRepository(session_maker)
133
+ project = await project_repository.get_by_name(config.project)
134
+ if not project: # pragma: no cover
135
+ raise Exception(f"Project '{config.project}' not found")
136
+
137
+ sync_service = await get_sync_service(project)
127
138
  knowledge_changes = await sync_service.scan(config.home)
128
- display_changes("Status", knowledge_changes, verbose)
139
+ display_changes(project.name, "Status", knowledge_changes, verbose)
129
140
 
130
141
 
131
142
  @app.command()
@@ -134,9 +145,8 @@ def status(
134
145
  ):
135
146
  """Show sync status between files and database."""
136
147
  try:
137
- sync_service = asyncio.run(get_sync_service())
138
- asyncio.run(run_status(sync_service, verbose)) # pragma: no cover
148
+ asyncio.run(run_status(verbose)) # pragma: no cover
139
149
  except Exception as e:
140
- logger.exception(f"Error checking status: {e}")
150
+ logger.error(f"Error checking status: {e}")
141
151
  typer.echo(f"Error checking status: {e}", err=True)
142
152
  raise typer.Exit(code=1) # pragma: no cover