diff --git a/src/basic_memory/cli/commands/project.py b/src/basic_memory/cli/commands/project.py index 4c63126d..4297a341 100644 --- a/src/basic_memory/cli/commands/project.py +++ b/src/basic_memory/cli/commands/project.py @@ -9,7 +9,6 @@ from rich.console import Console from rich.table import Table from basic_memory.cli.app import app -from basic_memory.config import config from basic_memory.mcp.project_session import session from basic_memory.mcp.resources.project_info import project_info import json @@ -24,6 +23,7 @@ from basic_memory.mcp.tools.utils import call_post from basic_memory.schemas.project_info import ProjectStatusResponse from basic_memory.mcp.tools.utils import call_delete from basic_memory.mcp.tools.utils import call_put +from basic_memory.utils import generate_permalink console = Console() @@ -44,11 +44,8 @@ def format_path(path: str) -> str: def list_projects() -> None: """List all configured projects.""" # Use API to list projects - - project_url = config.project_url - try: - response = asyncio.run(call_get(client, f"{project_url}/project/projects")) + response = asyncio.run(call_get(client, "/projects/projects")) result = ProjectList.model_validate(response.json()) table = Table(title="Basic Memory Projects") @@ -65,7 +62,6 @@ def list_projects() -> None: console.print(table) except Exception as e: console.print(f"[red]Error listing projects: {str(e)}[/red]") - console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]") raise typer.Exit(1) @@ -80,16 +76,14 @@ def add_project( resolved_path = os.path.abspath(os.path.expanduser(path)) try: - project_url = config.project_url data = {"name": name, "path": resolved_path, "set_default": set_default} - response = asyncio.run(call_post(client, f"{project_url}/project/projects", json=data)) + response = asyncio.run(call_post(client, "/projects/projects", json=data)) result = ProjectStatusResponse.model_validate(response.json()) console.print(f"[green]{result.message}[/green]") except Exception as e: console.print(f"[red]Error adding project: {str(e)}[/red]") - console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]") raise typer.Exit(1) # Display usage hint @@ -105,15 +99,13 @@ def remove_project( ) -> None: """Remove a project from configuration.""" try: - project_url = config.project_url - - response = asyncio.run(call_delete(client, f"{project_url}/project/projects/{name}")) + project_name = generate_permalink(name) + response = asyncio.run(call_delete(client, f"/projects/{project_name}")) result = ProjectStatusResponse.model_validate(response.json()) console.print(f"[green]{result.message}[/green]") except Exception as e: console.print(f"[red]Error removing project: {str(e)}[/red]") - console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]") raise typer.Exit(1) # Show this message regardless of method used @@ -126,20 +118,16 @@ def set_default_project( ) -> None: """Set the default project and activate it for the current session.""" try: - project_url = config.project_url + project_name = generate_permalink(name) - response = asyncio.run(call_put(client, f"{project_url}/project/projects/{name}/default")) + response = asyncio.run(call_put(client, f"projects/{project_name}/default")) result = ProjectStatusResponse.model_validate(response.json()) console.print(f"[green]{result.message}[/green]") except Exception as e: console.print(f"[red]Error setting default project: {str(e)}[/red]") - console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]") raise typer.Exit(1) - # Always activate it for the current session - os.environ["BASIC_MEMORY_PROJECT"] = name - # Reload configuration to apply the change from importlib import reload from basic_memory import config as config_module @@ -149,21 +137,18 @@ def set_default_project( console.print("[green]Project activated for current session[/green]") -@project_app.command("sync") +@project_app.command("sync-config") def synchronize_projects() -> None: - """Synchronize projects between configuration file and database.""" + """Synchronize project config between configuration file and database.""" # Call the API to synchronize projects - project_url = config.project_url - try: - response = asyncio.run(call_post(client, f"{project_url}/project/sync")) + response = asyncio.run(call_post(client, "/projects/sync")) result = ProjectStatusResponse.model_validate(response.json()) console.print(f"[green]{result.message}[/green]") except Exception as e: # pragma: no cover console.print(f"[red]Error synchronizing projects: {str(e)}[/red]") - console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]") raise typer.Exit(1) diff --git a/src/basic_memory/mcp/tools/project_management.py b/src/basic_memory/mcp/tools/project_management.py index bcc72f08..a9a72ff0 100644 --- a/src/basic_memory/mcp/tools/project_management.py +++ b/src/basic_memory/mcp/tools/project_management.py @@ -16,6 +16,7 @@ from basic_memory.mcp.server import mcp from basic_memory.mcp.tools.utils import call_get, call_put, call_post, call_delete from basic_memory.schemas import ProjectInfoResponse from basic_memory.schemas.project_info import ProjectList, ProjectStatusResponse, ProjectInfoRequest +from basic_memory.utils import generate_permalink @mcp.tool() @@ -77,6 +78,7 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str: if ctx: # pragma: no cover await ctx.info(f"Switching to project: {project_name}") + project_permalink = generate_permalink(project_name) current_project = session.get_current_project() try: # Validate project exists by getting project list @@ -84,13 +86,13 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str: project_list = ProjectList.model_validate(response.json()) # Check if project exists - project_exists = any(p.name == project_name for p in project_list.projects) + project_exists = any(p.permalink == project_permalink for p in project_list.projects) if not project_exists: available_projects = [p.name for p in project_list.projects] return f"Error: Project '{project_name}' not found. Available projects: {', '.join(available_projects)}" # Switch to the project - session.set_current_project(project_name) + session.set_current_project(project_permalink) current_project = session.get_current_project() project_config = get_project_config(current_project) @@ -99,11 +101,11 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str: response = await call_get( client, f"{project_config.project_url}/project/info", - params={"project_name": project_name}, + params={"project_name": project_permalink}, ) project_info = ProjectInfoResponse.model_validate(response.json()) - result = f"✓ Switched to {project_name} project\n\n" + result = f"✓ Switched to {project_permalink} project\n\n" result += "Project Summary:\n" result += f"• {project_info.statistics.total_entities} entities\n" result += f"• {project_info.statistics.total_observations} observations\n" @@ -329,4 +331,4 @@ async def delete_project(project_name: str, ctx: Context | None = None) -> str: result += "Files remain on disk but project is no longer tracked by Basic Memory.\n" result += "Re-add the project to access its content again.\n" - return add_project_metadata(result, session.get_current_project()) + return add_project_metadata(result, session.get_current_project()) \ No newline at end of file diff --git a/src/basic_memory/schemas/project_info.py b/src/basic_memory/schemas/project_info.py index 2ac8053d..8072431b 100644 --- a/src/basic_memory/schemas/project_info.py +++ b/src/basic_memory/schemas/project_info.py @@ -6,6 +6,8 @@ from typing import Dict, List, Optional, Any from pydantic import Field, BaseModel +from basic_memory.utils import generate_permalink + class ProjectStatistics(BaseModel): """Statistics about the current project.""" @@ -183,6 +185,10 @@ class ProjectItem(BaseModel): name: str path: str is_default: bool = False + + @property + def permalink(self) -> str: # pragma: no cover + return generate_permalink(self.name) class ProjectList(BaseModel): diff --git a/src/basic_memory/services/project_service.py b/src/basic_memory/services/project_service.py index e677ea7e..8f76db5d 100644 --- a/src/basic_memory/services/project_service.py +++ b/src/basic_memory/services/project_service.py @@ -207,7 +207,7 @@ class ProjectService: # Get all projects from database db_projects = await self.repository.get_active_projects() - db_projects_by_name = {p.name: p for p in db_projects} + db_projects_by_permalink = {p.permalink: p for p in db_projects} # Get all projects from configuration and normalize names if needed config_projects = config_manager.projects.copy() @@ -235,7 +235,7 @@ class ProjectService: # Add projects that exist in config but not in DB for name, path in config_projects.items(): - if name not in db_projects_by_name: + if name not in db_projects_by_permalink: logger.info(f"Adding project '{name}' to database") project_data = { "name": name, @@ -247,7 +247,7 @@ class ProjectService: await self.repository.create(project_data) # Add projects that exist in DB but not in config to config - for name, project in db_projects_by_name.items(): + for name, project in db_projects_by_permalink.items(): if name not in config_projects: logger.info(f"Adding project '{name}' to configuration") config_manager.add_project(name, project.path) @@ -668,4 +668,4 @@ class ProjectService: database_size=db_size_readable, watch_status=watch_status, timestamp=datetime.now(), - ) + ) \ No newline at end of file diff --git a/tests/cli/test_project_commands.py b/tests/cli/test_project_commands.py index f64cf120..4af7c9ae 100644 --- a/tests/cli/test_project_commands.py +++ b/tests/cli/test_project_commands.py @@ -93,8 +93,6 @@ def test_project_default_command(mock_reload, mock_run, cli_env): # Just verify it runs without exception and environment is set assert result.exit_code == 0 - assert "BASIC_MEMORY_PROJECT" in os.environ - assert os.environ["BASIC_MEMORY_PROJECT"] == "test-project" @patch("basic_memory.cli.commands.project.asyncio.run") @@ -111,7 +109,7 @@ def test_project_sync_command(mock_run, cli_env): mock_run.return_value = mock_response runner = CliRunner() - result = runner.invoke(cli_app, ["project", "sync"]) + result = runner.invoke(cli_app, ["project", "sync-config"]) # Just verify it runs without exception assert result.exit_code == 0 @@ -134,7 +132,6 @@ def test_project_failure_exits_with_error(mock_run, cli_env): # All should exit with code 1 and show error message assert list_result.exit_code == 1 assert "Error listing projects" in list_result.output - assert "Make sure the Basic Memory server is running" in list_result.output assert add_result.exit_code == 1 assert "Error adding project" in add_result.output diff --git a/tests/conftest.py b/tests/conftest.py index a989a707..c8f7d8fd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -105,7 +105,6 @@ def config_manager( ) # Patch the project config that CLI commands import (only modules that actually import config) - monkeypatch.setattr("basic_memory.cli.commands.project.config", project_config) monkeypatch.setattr("basic_memory.cli.commands.sync.config", project_config) monkeypatch.setattr("basic_memory.cli.commands.status.config", project_config) monkeypatch.setattr("basic_memory.cli.commands.import_memory_json.config", project_config)