fix: double-default display in project list + stale test updates

Use config.default_project as single source of truth for the Default
column in `bm project list`, removing checks against local DB and cloud
API is_default fields that could independently mark multiple projects.

Also updates tests that were out of date after get_project_mode changed
to default unknown projects to CLOUD:
- test_get_client_local_project_uses_asgi_transport: register "main" as LOCAL
- test_run_filters_cloud_projects_each_cycle: register local project in config
- test_new_project_addition_scenario: register projects as LOCAL in config
- test_get_project_mode_defaults_to_cloud: assert new CLOUD default

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
phernandez
2026-02-22 00:33:25 -06:00
parent 56cefbaafd
commit 6ff39076a0
9 changed files with 121 additions and 201 deletions
+21 -9
View File
@@ -161,13 +161,7 @@ def list_projects(
else:
cli_route = ProjectMode.LOCAL.value
is_default = ""
if config.default_project == project_name:
is_default = "[X]"
if local_project is not None and local_project.is_default:
is_default = "[X]"
if cloud_project is not None and cloud_project.is_default:
is_default = "[X]"
is_default = "[X]" if config.default_project == project_name else ""
has_sync = "[X]" if entry and entry.local_sync_path else ""
mcp_stdio_target = "local" if local_project is not None else "n/a"
@@ -326,7 +320,16 @@ def remove_project(
raise typer.Exit(1)
async def _remove_project():
async with get_client() as client:
# Resolve workspace so cloud-only projects auto-route without --cloud
config = ConfigManager().config
entry = config.projects.get(name)
ws = None
if entry and entry.workspace_id:
ws = entry.workspace_id
elif config.default_workspace:
ws = config.default_workspace
async with get_client(project_name=name, workspace=ws) as client:
project_client = ProjectClient(client)
# Convert name to permalink for efficient resolution
project_permalink = generate_permalink(name)
@@ -405,7 +408,16 @@ def set_default_project(
"""
async def _set_default():
async with get_client() as client:
# Resolve workspace so cloud-only projects auto-route without flags
config = ConfigManager().config
entry = config.projects.get(name)
ws = None
if entry and entry.workspace_id:
ws = entry.workspace_id
elif config.default_workspace:
ws = config.default_workspace
async with get_client(project_name=name, workspace=ws) as client:
project_client = ProjectClient(client)
# Convert name to permalink for efficient resolution
project_permalink = generate_permalink(name)
+9 -47
View File
@@ -14,7 +14,6 @@ from loguru import logger
from basic_memory.cli.app import app
from basic_memory.cli.commands.command_utils import run_with_cleanup
from basic_memory.cli.commands.routing import force_routing, validate_routing_flags
from basic_memory.config import ConfigManager
from basic_memory.mcp.tools import build_context as mcp_build_context
from basic_memory.mcp.tools import edit_note as mcp_edit_note
from basic_memory.mcp.tools import list_memory_projects as mcp_list_projects
@@ -36,16 +35,6 @@ VALID_EDIT_OPERATIONS = ["append", "prepend", "find_replace", "replace_section"]
# --- Shared helpers ---
def _resolve_project(config_manager: ConfigManager, project: Optional[str]) -> Optional[str]:
"""Resolve project name from CLI arg or config default."""
if project is not None:
project_name, _ = config_manager.get_project(project)
if not project_name:
raise ValueError(f"No project found named: {project}")
return project_name
return config_manager.default_project
def _print_json(result: Any) -> None:
"""Print a result as formatted JSON."""
print(json.dumps(result, indent=2, ensure_ascii=True, default=str))
@@ -108,9 +97,6 @@ def write_note(
typer.echo("Empty content provided. Please provide non-empty content.", err=True)
raise typer.Exit(1)
config_manager = ConfigManager()
project_name = _resolve_project(config_manager, project)
assert content is not None
with force_routing(local=local, cloud=cloud):
@@ -119,7 +105,7 @@ def write_note(
title=title,
content=content,
directory=folder,
project=project_name,
project=project,
workspace=workspace,
tags=tags,
output_format="json",
@@ -168,14 +154,11 @@ def read_note(
try:
validate_routing_flags(local, cloud)
config_manager = ConfigManager()
project_name = _resolve_project(config_manager, project)
with force_routing(local=local, cloud=cloud):
result = run_with_cleanup(
mcp_read_note(
identifier=identifier,
project=project_name,
project=project,
workspace=workspace,
page=page,
page_size=page_size,
@@ -237,16 +220,13 @@ def edit_note(
try:
validate_routing_flags(local, cloud)
config_manager = ConfigManager()
project_name = _resolve_project(config_manager, project)
with force_routing(local=local, cloud=cloud):
result = run_with_cleanup(
mcp_edit_note(
identifier=identifier,
operation=operation,
content=content,
project=project_name,
project=project,
workspace=workspace,
section=section,
find_text=find_text,
@@ -304,14 +284,11 @@ def build_context(
try:
validate_routing_flags(local, cloud)
config_manager = ConfigManager()
project_name = _resolve_project(config_manager, project)
with force_routing(local=local, cloud=cloud):
result = run_with_cleanup(
mcp_build_context(
url=url,
project=project_name,
project=project,
workspace=workspace,
depth=depth,
timeframe=timeframe,
@@ -365,9 +342,6 @@ def recent_activity(
try:
validate_routing_flags(local, cloud)
config_manager = ConfigManager()
project_name = _resolve_project(config_manager, project)
with force_routing(local=local, cloud=cloud):
result = run_with_cleanup(
mcp_recent_activity(
@@ -376,7 +350,7 @@ def recent_activity(
timeframe=timeframe if timeframe is not None else "7d",
page=page,
page_size=page_size,
project=project_name,
project=project,
workspace=workspace,
output_format="json",
)
@@ -460,9 +434,6 @@ def search_notes(
try:
validate_routing_flags(local, cloud)
config_manager = ConfigManager()
project_name = _resolve_project(config_manager, project)
mode_flags = [permalink, title, vector, hybrid]
if sum(1 for enabled in mode_flags if enabled) > 1: # pragma: no cover
typer.echo(
@@ -515,7 +486,7 @@ def search_notes(
result = run_with_cleanup(
mcp_search(
query=query or "",
project=project_name,
project=project,
workspace=workspace,
search_type=search_type,
output_format="json",
@@ -649,9 +620,6 @@ def schema_validate(
try:
validate_routing_flags(local, cloud)
config_manager = ConfigManager()
project_name = _resolve_project(config_manager, project)
# Heuristic: if target contains / or ., treat as identifier; otherwise as note type
note_type, identifier = None, None
if target:
@@ -665,7 +633,7 @@ def schema_validate(
mcp_schema_validate(
note_type=note_type,
identifier=identifier,
project=project_name,
project=project,
workspace=workspace,
output_format="json",
)
@@ -717,15 +685,12 @@ def schema_infer(
try:
validate_routing_flags(local, cloud)
config_manager = ConfigManager()
project_name = _resolve_project(config_manager, project)
with force_routing(local=local, cloud=cloud):
result = run_with_cleanup(
mcp_schema_infer(
note_type=entity_type,
threshold=threshold,
project=project_name,
project=project,
workspace=workspace,
output_format="json",
)
@@ -773,14 +738,11 @@ def schema_diff(
try:
validate_routing_flags(local, cloud)
config_manager = ConfigManager()
project_name = _resolve_project(config_manager, project)
with force_routing(local=local, cloud=cloud):
result = run_with_cleanup(
mcp_schema_diff(
note_type=entity_type,
project=project_name,
project=project,
workspace=workspace,
output_format="json",
)
+4 -2
View File
@@ -425,10 +425,12 @@ class BasicMemoryConfig(BaseSettings):
def get_project_mode(self, project_name: str) -> ProjectMode:
"""Get the routing mode for a project.
Returns the per-project mode if set, otherwise LOCAL.
Returns the per-project mode if set.
Unknown projects (not in local config) default to CLOUD —
local projects are always registered in config.
"""
entry = self.projects.get(project_name)
return entry.mode if entry else ProjectMode.LOCAL
return entry.mode if entry else ProjectMode.CLOUD
def set_project_mode(self, project_name: str, mode: ProjectMode) -> None:
"""Set the routing mode for a project.
+1 -1
View File
@@ -445,6 +445,7 @@ async def get_project_client(
# Step 3: Determine if cloud routing is needed
config = ConfigManager().config
project_entry = config.projects.get(resolved_project)
project_mode = config.get_project_mode(resolved_project)
# Trigger: workspace provided for a local project (without explicit --cloud)
@@ -459,7 +460,6 @@ async def get_project_client(
if project_mode == ProjectMode.CLOUD or (_explicit_routing() and not _force_local_mode()):
# --- Cloud routing: resolve workspace with priority chain ---
effective_workspace = workspace
project_entry = config.projects.get(resolved_project)
# Priority 2: per-project workspace_id from config
if effective_workspace is None and project_entry and project_entry.workspace_id: