44import os
55from datetime import datetime
66from pathlib import Path
7+ from typing import cast
78
89import typer
910from rich .console import Console , Group
2728from basic_memory .config import ConfigManager , ProjectEntry , ProjectMode
2829from basic_memory .mcp .async_client import get_client
2930from basic_memory .mcp .clients import ProjectClient
31+ from basic_memory .schemas .cloud import ProjectVisibility
3032from basic_memory .schemas .project_info import ProjectItem , ProjectList
3133from basic_memory .utils import generate_permalink , normalize_project_path
3234
@@ -56,27 +58,27 @@ def make_bar(value: int, max_value: int, width: int = 40) -> Text:
5658 return bar
5759
5860
59- def _normalize_project_visibility (visibility : str | None ) -> str :
61+ def _normalize_project_visibility (visibility : str | None ) -> ProjectVisibility :
6062 """Normalize CLI visibility input to the cloud API contract."""
6163 if visibility is None :
6264 return "workspace"
6365
6466 normalized = visibility .strip ().lower ()
6567 if normalized in {"workspace" , "shared" , "private" }:
66- return normalized
68+ return cast ( ProjectVisibility , normalized )
6769
6870 raise ValueError ("Invalid visibility. Expected one of: workspace, shared, private." )
6971
7072
7173def _resolve_workspace_id (config , workspace : str | None ) -> str | None :
7274 """Resolve a workspace name or tenant_id to a tenant_id."""
73- if workspace is not None :
74- from basic_memory .mcp .project_context import (
75- _workspace_choices ,
76- _workspace_matches_identifier ,
77- get_available_workspaces ,
78- )
75+ from basic_memory .mcp .project_context import (
76+ _workspace_choices ,
77+ _workspace_matches_identifier ,
78+ get_available_workspaces ,
79+ )
7980
81+ if workspace is not None :
8082 workspaces = run_with_cleanup (get_available_workspaces ())
8183 matches = [ws for ws in workspaces if _workspace_matches_identifier (ws , workspace )]
8284 if not matches :
@@ -97,8 +99,6 @@ def _resolve_workspace_id(config, workspace: str | None) -> str | None:
9799 return config .default_workspace
98100
99101 try :
100- from basic_memory .mcp .project_context import get_available_workspaces
101-
102102 workspaces = run_with_cleanup (get_available_workspaces ())
103103 if len (workspaces ) == 1 :
104104 return workspaces [0 ].tenant_id
@@ -402,28 +402,34 @@ async def _add_project():
402402 result = run_with_cleanup (_add_project ())
403403 console .print (f"[green]{ result .message } [/green]" )
404404
405- # Save local sync path to config if in cloud mode
406- if effective_cloud_mode and local_sync_path :
407- # Create local directory if it doesn't exist
408- local_dir = Path (local_sync_path )
409- local_dir .mkdir (parents = True , exist_ok = True )
410-
411- # Update project entry — path is always the local directory
405+ # Trigger: local config needs enough metadata to route future commands back to cloud.
406+ # Why: explicit workspace selection and local sync state should persist across CLI sessions.
407+ # Outcome: cloud-backed projects keep cloud mode, workspace_id, and optional local sync path.
408+ if effective_cloud_mode and (local_sync_path or resolved_workspace_id ):
412409 entry = config .projects .get (name )
413410 if entry :
414- entry .path = local_sync_path
415- entry .local_sync_path = local_sync_path
411+ entry .mode = ProjectMode .CLOUD
412+ if local_sync_path :
413+ entry .path = local_sync_path
414+ entry .local_sync_path = local_sync_path
416415 if resolved_workspace_id :
417416 entry .workspace_id = resolved_workspace_id
418417 else :
419418 # Project may not be in local config yet (cloud-only add)
420419 config .projects [name ] = ProjectEntry (
421- path = local_sync_path ,
420+ path = local_sync_path or "" ,
421+ mode = ProjectMode .CLOUD ,
422422 local_sync_path = local_sync_path ,
423423 workspace_id = resolved_workspace_id ,
424424 )
425425 ConfigManager ().save_config (config )
426426
427+ # Save local sync path to config if in cloud mode
428+ if effective_cloud_mode and local_sync_path :
429+ # Create local directory if it doesn't exist
430+ local_dir = Path (local_sync_path )
431+ local_dir .mkdir (parents = True , exist_ok = True )
432+
427433 console .print (f"\n [green]Local sync path configured: { local_sync_path } [/green]" )
428434 console .print ("\n Next steps:" )
429435 console .print (f" 1. Preview: bm cloud bisync --name { name } --resync --dry-run" )
0 commit comments