mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
fix: project cli commands and case sensitivity when switching projects (#130)
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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())
|
||||
@@ -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):
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user