Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/basic_memory/cli/commands/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,12 @@ async def _list_projects(ws: str | None = None):
if cloud_project is not None and cloud_ws_name:
ws_label = f"{cloud_ws_name} ({cloud_ws_type})" if cloud_ws_type else cloud_ws_name

# Use display_name from cloud response (e.g., "My Project" for private UUID-named projects)
display_name = (
cloud_project.display_name if cloud_project and cloud_project.display_name else None
)
row_data = {
"name": project_name,
"name": display_name or project_name,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve canonical project identifier in list output

Replacing row_data["name"] with display_name makes bm project list --json emit a human label instead of the canonical identifier for private cloud projects, but other project commands still resolve by canonical name/permalink (src/basic_memory/cli/commands/project.py lines 480-482 and 568-570 call generate_permalink(name) before resolve; src/basic_memory/api/v2/routers/project_router.py lines 250-269 match against stored project.name). For UUID-backed private projects, piping .projects[].name from list output into project info/remove/default can no longer resolve the project. This should keep name canonical and expose display_name separately (or limit display substitution to the Rich table only).

Useful? React with 👍 / 👎.

"permalink": permalink,
"local_path": local_path,
"cloud_path": cloud_path,
Expand Down
2 changes: 1 addition & 1 deletion src/basic_memory/mcp/clients/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ async def list_projects(self) -> ProjectList:
"""
response = await call_get(
self.http_client,
"/v2/projects/",
"/v2/projects",
)
return ProjectList.model_validate(response.json())

Expand Down
71 changes: 71 additions & 0 deletions tests/cli/test_project_list_and_ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,77 @@ async def fake_list_projects(self):
assert "/beta" in result.stdout


def test_project_list_shows_display_name_for_private_projects(
runner: CliRunner, write_config, mock_client, tmp_path, monkeypatch
):
"""Private projects should show display_name ('My Project') instead of raw UUID name."""
private_uuid = "f1df8f39-d5aa-4095-ae05-8c5a2883029a"

write_config(
{
"env": "dev",
"projects": {},
"default_project": "main",
"cloud_api_key": "bmc_test_key_123",
}
)

local_payload = {
"projects": [
{
"id": 1,
"external_id": "11111111-1111-1111-1111-111111111111",
"name": "main",
"path": "/main",
"is_default": True,
}
],
"default_project": "main",
}

cloud_payload = {
"projects": [
{
"id": 1,
"external_id": "11111111-1111-1111-1111-111111111111",
"name": "main",
"path": "/main",
"is_default": True,
},
{
"id": 2,
"external_id": "22222222-2222-2222-2222-222222222222",
"name": private_uuid,
"path": f"/{private_uuid}",
"is_default": False,
"display_name": "My Project",
"is_private": True,
},
],
"default_project": "main",
}

async def fake_list_projects(self):
if os.getenv("BASIC_MEMORY_FORCE_CLOUD", "").lower() in ("true", "1", "yes"):
return ProjectList.model_validate(cloud_payload)
return ProjectList.model_validate(local_payload)

monkeypatch.setattr(ProjectClient, "list_projects", fake_list_projects)

result = runner.invoke(app, ["project", "list"], env={"COLUMNS": "240"})

assert result.exit_code == 0, f"Exit code: {result.exit_code}, output: {result.stdout}"
# display_name should appear in the Name column instead of the raw UUID
assert "My Project" in result.stdout
# The Name column should show "My Project", not the UUID.
# The UUID may still appear in the Cloud Path column — that's expected.
lines = result.stdout.splitlines()
project_line = next(line for line in lines if "My Project" in line)
# The name cell is the first column — verify UUID is not the displayed name
name_cell = project_line.split("│")[1].strip()
assert name_cell == "My Project"


def test_project_ls_local_mode_defaults_to_local_route(
runner: CliRunner, write_config, mock_client, tmp_path, monkeypatch
):
Expand Down
Loading