Skip to content

Commit 9e0385d

Browse files
jope-bmclaude
andcommitted
fix(cli): preserve canonical name in JSON, use display_name only in table
Keep row_data["name"] as the canonical project identifier (UUID for private projects) so that `bm project list --json` output can be piped into other commands. The human-friendly display_name is added as a separate field and used only in the Rich table rendering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Joe P <joe@basicmemory.com>
1 parent 8907e48 commit 9e0385d

2 files changed

Lines changed: 16 additions & 7 deletions

File tree

src/basic_memory/cli/commands/project.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,12 +253,14 @@ async def _list_projects(ws: str | None = None):
253253
if cloud_project is not None and cloud_ws_name:
254254
ws_label = f"{cloud_ws_name} ({cloud_ws_type})" if cloud_ws_type else cloud_ws_name
255255

256-
# Use display_name from cloud response (e.g., "My Project" for private UUID-named projects)
256+
# display_name is a human label for private UUID-named projects (e.g., "My Project").
257+
# Keep "name" as the canonical identifier for scripting/JSON consumers;
258+
# the Rich table uses display_name when available.
257259
display_name = (
258260
cloud_project.display_name if cloud_project and cloud_project.display_name else None
259261
)
260262
row_data = {
261-
"name": display_name or project_name,
263+
"name": project_name,
262264
"permalink": permalink,
263265
"local_path": local_path,
264266
"cloud_path": cloud_path,
@@ -267,6 +269,8 @@ async def _list_projects(ws: str | None = None):
267269
"sync": has_sync,
268270
"is_default": is_default,
269271
}
272+
if display_name:
273+
row_data["display_name"] = display_name
270274
if ws_label:
271275
row_data["workspace"] = cloud_ws_name or ""
272276
if cloud_ws_type:
@@ -282,7 +286,7 @@ async def _list_projects(ws: str | None = None):
282286
# --- Rich table output ---
283287
for row_data in project_rows:
284288
table.add_row(
285-
row_data["name"],
289+
row_data.get("display_name") or row_data["name"],
286290
row_data["local_path"],
287291
row_data["cloud_path"],
288292
row_data.get("workspace", "")

tests/cli/test_project_list_and_ls.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,16 +199,21 @@ async def fake_list_projects(self):
199199
result = runner.invoke(app, ["project", "list"], env={"COLUMNS": "240"})
200200

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

209+
# JSON output should preserve canonical name for scripting, with display_name as separate field
210+
json_result = runner.invoke(app, ["project", "list", "--json"], env={"COLUMNS": "240"})
211+
assert json_result.exit_code == 0
212+
data = json.loads(json_result.stdout)
213+
private_project = next(p for p in data["projects"] if p.get("display_name") == "My Project")
214+
assert private_project["name"] == private_uuid
215+
assert private_project["display_name"] == "My Project"
216+
212217

213218
def test_project_ls_local_mode_defaults_to_local_route(
214219
runner: CliRunner, write_config, mock_client, tmp_path, monkeypatch

0 commit comments

Comments
 (0)