Skip to content

Commit 3ee30e1

Browse files
authored
fix: project cli commands and case sensitivity when switching projects (#130)
Signed-off-by: phernandez <[email protected]>
1 parent ac401ea commit 3ee30e1

File tree

6 files changed

+28
-39
lines changed

6 files changed

+28
-39
lines changed

src/basic_memory/cli/commands/project.py

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from rich.table import Table
1010

1111
from basic_memory.cli.app import app
12-
from basic_memory.config import config
1312
from basic_memory.mcp.project_session import session
1413
from basic_memory.mcp.resources.project_info import project_info
1514
import json
@@ -24,6 +23,7 @@
2423
from basic_memory.schemas.project_info import ProjectStatusResponse
2524
from basic_memory.mcp.tools.utils import call_delete
2625
from basic_memory.mcp.tools.utils import call_put
26+
from basic_memory.utils import generate_permalink
2727

2828
console = Console()
2929

@@ -44,11 +44,8 @@ def format_path(path: str) -> str:
4444
def list_projects() -> None:
4545
"""List all configured projects."""
4646
# Use API to list projects
47-
48-
project_url = config.project_url
49-
5047
try:
51-
response = asyncio.run(call_get(client, f"{project_url}/project/projects"))
48+
response = asyncio.run(call_get(client, "/projects/projects"))
5249
result = ProjectList.model_validate(response.json())
5350

5451
table = Table(title="Basic Memory Projects")
@@ -65,7 +62,6 @@ def list_projects() -> None:
6562
console.print(table)
6663
except Exception as e:
6764
console.print(f"[red]Error listing projects: {str(e)}[/red]")
68-
console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]")
6965
raise typer.Exit(1)
7066

7167

@@ -80,16 +76,14 @@ def add_project(
8076
resolved_path = os.path.abspath(os.path.expanduser(path))
8177

8278
try:
83-
project_url = config.project_url
8479
data = {"name": name, "path": resolved_path, "set_default": set_default}
8580

86-
response = asyncio.run(call_post(client, f"{project_url}/project/projects", json=data))
81+
response = asyncio.run(call_post(client, "/projects/projects", json=data))
8782
result = ProjectStatusResponse.model_validate(response.json())
8883

8984
console.print(f"[green]{result.message}[/green]")
9085
except Exception as e:
9186
console.print(f"[red]Error adding project: {str(e)}[/red]")
92-
console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]")
9387
raise typer.Exit(1)
9488

9589
# Display usage hint
@@ -105,15 +99,13 @@ def remove_project(
10599
) -> None:
106100
"""Remove a project from configuration."""
107101
try:
108-
project_url = config.project_url
109-
110-
response = asyncio.run(call_delete(client, f"{project_url}/project/projects/{name}"))
102+
project_name = generate_permalink(name)
103+
response = asyncio.run(call_delete(client, f"/projects/{project_name}"))
111104
result = ProjectStatusResponse.model_validate(response.json())
112105

113106
console.print(f"[green]{result.message}[/green]")
114107
except Exception as e:
115108
console.print(f"[red]Error removing project: {str(e)}[/red]")
116-
console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]")
117109
raise typer.Exit(1)
118110

119111
# Show this message regardless of method used
@@ -126,20 +118,16 @@ def set_default_project(
126118
) -> None:
127119
"""Set the default project and activate it for the current session."""
128120
try:
129-
project_url = config.project_url
121+
project_name = generate_permalink(name)
130122

131-
response = asyncio.run(call_put(client, f"{project_url}/project/projects/{name}/default"))
123+
response = asyncio.run(call_put(client, f"projects/{project_name}/default"))
132124
result = ProjectStatusResponse.model_validate(response.json())
133125

134126
console.print(f"[green]{result.message}[/green]")
135127
except Exception as e:
136128
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]")
138129
raise typer.Exit(1)
139130

140-
# Always activate it for the current session
141-
os.environ["BASIC_MEMORY_PROJECT"] = name
142-
143131
# Reload configuration to apply the change
144132
from importlib import reload
145133
from basic_memory import config as config_module
@@ -149,21 +137,18 @@ def set_default_project(
149137
console.print("[green]Project activated for current session[/green]")
150138

151139

152-
@project_app.command("sync")
140+
@project_app.command("sync-config")
153141
def synchronize_projects() -> None:
154-
"""Synchronize projects between configuration file and database."""
142+
"""Synchronize project config between configuration file and database."""
155143
# Call the API to synchronize projects
156144

157-
project_url = config.project_url
158-
159145
try:
160-
response = asyncio.run(call_post(client, f"{project_url}/project/sync"))
146+
response = asyncio.run(call_post(client, "/projects/sync"))
161147
result = ProjectStatusResponse.model_validate(response.json())
162148

163149
console.print(f"[green]{result.message}[/green]")
164150
except Exception as e: # pragma: no cover
165151
console.print(f"[red]Error synchronizing projects: {str(e)}[/red]")
166-
console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]")
167152
raise typer.Exit(1)
168153

169154

src/basic_memory/mcp/tools/project_management.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from basic_memory.mcp.tools.utils import call_get, call_put, call_post, call_delete
1717
from basic_memory.schemas import ProjectInfoResponse
1818
from basic_memory.schemas.project_info import ProjectList, ProjectStatusResponse, ProjectInfoRequest
19+
from basic_memory.utils import generate_permalink
1920

2021

2122
@mcp.tool()
@@ -77,20 +78,21 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str:
7778
if ctx: # pragma: no cover
7879
await ctx.info(f"Switching to project: {project_name}")
7980

81+
project_permalink = generate_permalink(project_name)
8082
current_project = session.get_current_project()
8183
try:
8284
# Validate project exists by getting project list
8385
response = await call_get(client, "/projects/projects")
8486
project_list = ProjectList.model_validate(response.json())
8587

8688
# Check if project exists
87-
project_exists = any(p.name == project_name for p in project_list.projects)
89+
project_exists = any(p.permalink == project_permalink for p in project_list.projects)
8890
if not project_exists:
8991
available_projects = [p.name for p in project_list.projects]
9092
return f"Error: Project '{project_name}' not found. Available projects: {', '.join(available_projects)}"
9193

9294
# Switch to the project
93-
session.set_current_project(project_name)
95+
session.set_current_project(project_permalink)
9496
current_project = session.get_current_project()
9597
project_config = get_project_config(current_project)
9698

@@ -99,11 +101,11 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str:
99101
response = await call_get(
100102
client,
101103
f"{project_config.project_url}/project/info",
102-
params={"project_name": project_name},
104+
params={"project_name": project_permalink},
103105
)
104106
project_info = ProjectInfoResponse.model_validate(response.json())
105107

106-
result = f"✓ Switched to {project_name} project\n\n"
108+
result = f"✓ Switched to {project_permalink} project\n\n"
107109
result += "Project Summary:\n"
108110
result += f"• {project_info.statistics.total_entities} entities\n"
109111
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:
329331
result += "Files remain on disk but project is no longer tracked by Basic Memory.\n"
330332
result += "Re-add the project to access its content again.\n"
331333

332-
return add_project_metadata(result, session.get_current_project())
334+
return add_project_metadata(result, session.get_current_project())

src/basic_memory/schemas/project_info.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from pydantic import Field, BaseModel
88

9+
from basic_memory.utils import generate_permalink
10+
911

1012
class ProjectStatistics(BaseModel):
1113
"""Statistics about the current project."""
@@ -183,6 +185,10 @@ class ProjectItem(BaseModel):
183185
name: str
184186
path: str
185187
is_default: bool = False
188+
189+
@property
190+
def permalink(self) -> str: # pragma: no cover
191+
return generate_permalink(self.name)
186192

187193

188194
class ProjectList(BaseModel):

src/basic_memory/services/project_service.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ async def synchronize_projects(self) -> None: # pragma: no cover
207207

208208
# Get all projects from database
209209
db_projects = await self.repository.get_active_projects()
210-
db_projects_by_name = {p.name: p for p in db_projects}
210+
db_projects_by_permalink = {p.permalink: p for p in db_projects}
211211

212212
# Get all projects from configuration and normalize names if needed
213213
config_projects = config_manager.projects.copy()
@@ -235,7 +235,7 @@ async def synchronize_projects(self) -> None: # pragma: no cover
235235

236236
# Add projects that exist in config but not in DB
237237
for name, path in config_projects.items():
238-
if name not in db_projects_by_name:
238+
if name not in db_projects_by_permalink:
239239
logger.info(f"Adding project '{name}' to database")
240240
project_data = {
241241
"name": name,
@@ -247,7 +247,7 @@ async def synchronize_projects(self) -> None: # pragma: no cover
247247
await self.repository.create(project_data)
248248

249249
# Add projects that exist in DB but not in config to config
250-
for name, project in db_projects_by_name.items():
250+
for name, project in db_projects_by_permalink.items():
251251
if name not in config_projects:
252252
logger.info(f"Adding project '{name}' to configuration")
253253
config_manager.add_project(name, project.path)
@@ -668,4 +668,4 @@ def get_system_status(self) -> SystemStatus:
668668
database_size=db_size_readable,
669669
watch_status=watch_status,
670670
timestamp=datetime.now(),
671-
)
671+
)

tests/cli/test_project_commands.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,6 @@ def test_project_default_command(mock_reload, mock_run, cli_env):
9393

9494
# Just verify it runs without exception and environment is set
9595
assert result.exit_code == 0
96-
assert "BASIC_MEMORY_PROJECT" in os.environ
97-
assert os.environ["BASIC_MEMORY_PROJECT"] == "test-project"
9896

9997

10098
@patch("basic_memory.cli.commands.project.asyncio.run")
@@ -111,7 +109,7 @@ def test_project_sync_command(mock_run, cli_env):
111109
mock_run.return_value = mock_response
112110

113111
runner = CliRunner()
114-
result = runner.invoke(cli_app, ["project", "sync"])
112+
result = runner.invoke(cli_app, ["project", "sync-config"])
115113

116114
# Just verify it runs without exception
117115
assert result.exit_code == 0
@@ -134,7 +132,6 @@ def test_project_failure_exits_with_error(mock_run, cli_env):
134132
# All should exit with code 1 and show error message
135133
assert list_result.exit_code == 1
136134
assert "Error listing projects" in list_result.output
137-
assert "Make sure the Basic Memory server is running" in list_result.output
138135

139136
assert add_result.exit_code == 1
140137
assert "Error adding project" in add_result.output

tests/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ def mock_get_project_config(project_name=None):
105105
)
106106

107107
# Patch the project config that CLI commands import (only modules that actually import config)
108-
monkeypatch.setattr("basic_memory.cli.commands.project.config", project_config)
109108
monkeypatch.setattr("basic_memory.cli.commands.sync.config", project_config)
110109
monkeypatch.setattr("basic_memory.cli.commands.status.config", project_config)
111110
monkeypatch.setattr("basic_memory.cli.commands.import_memory_json.config", project_config)

0 commit comments

Comments
 (0)