Skip to content

Commit 6ff3907

Browse files
phernandezclaude
andcommitted
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>
1 parent 56cefba commit 6ff3907

9 files changed

Lines changed: 121 additions & 201 deletions

File tree

src/basic_memory/cli/commands/project.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,7 @@ async def _list_projects(ws: str | None = None):
161161
else:
162162
cli_route = ProjectMode.LOCAL.value
163163

164-
is_default = ""
165-
if config.default_project == project_name:
166-
is_default = "[X]"
167-
if local_project is not None and local_project.is_default:
168-
is_default = "[X]"
169-
if cloud_project is not None and cloud_project.is_default:
170-
is_default = "[X]"
164+
is_default = "[X]" if config.default_project == project_name else ""
171165

172166
has_sync = "[X]" if entry and entry.local_sync_path else ""
173167
mcp_stdio_target = "local" if local_project is not None else "n/a"
@@ -326,7 +320,16 @@ def remove_project(
326320
raise typer.Exit(1)
327321

328322
async def _remove_project():
329-
async with get_client() as client:
323+
# Resolve workspace so cloud-only projects auto-route without --cloud
324+
config = ConfigManager().config
325+
entry = config.projects.get(name)
326+
ws = None
327+
if entry and entry.workspace_id:
328+
ws = entry.workspace_id
329+
elif config.default_workspace:
330+
ws = config.default_workspace
331+
332+
async with get_client(project_name=name, workspace=ws) as client:
330333
project_client = ProjectClient(client)
331334
# Convert name to permalink for efficient resolution
332335
project_permalink = generate_permalink(name)
@@ -405,7 +408,16 @@ def set_default_project(
405408
"""
406409

407410
async def _set_default():
408-
async with get_client() as client:
411+
# Resolve workspace so cloud-only projects auto-route without flags
412+
config = ConfigManager().config
413+
entry = config.projects.get(name)
414+
ws = None
415+
if entry and entry.workspace_id:
416+
ws = entry.workspace_id
417+
elif config.default_workspace:
418+
ws = config.default_workspace
419+
420+
async with get_client(project_name=name, workspace=ws) as client:
409421
project_client = ProjectClient(client)
410422
# Convert name to permalink for efficient resolution
411423
project_permalink = generate_permalink(name)

src/basic_memory/cli/commands/tool.py

Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from basic_memory.cli.app import app
1515
from basic_memory.cli.commands.command_utils import run_with_cleanup
1616
from basic_memory.cli.commands.routing import force_routing, validate_routing_flags
17-
from basic_memory.config import ConfigManager
1817
from basic_memory.mcp.tools import build_context as mcp_build_context
1918
from basic_memory.mcp.tools import edit_note as mcp_edit_note
2019
from basic_memory.mcp.tools import list_memory_projects as mcp_list_projects
@@ -36,16 +35,6 @@
3635
# --- Shared helpers ---
3736

3837

39-
def _resolve_project(config_manager: ConfigManager, project: Optional[str]) -> Optional[str]:
40-
"""Resolve project name from CLI arg or config default."""
41-
if project is not None:
42-
project_name, _ = config_manager.get_project(project)
43-
if not project_name:
44-
raise ValueError(f"No project found named: {project}")
45-
return project_name
46-
return config_manager.default_project
47-
48-
4938
def _print_json(result: Any) -> None:
5039
"""Print a result as formatted JSON."""
5140
print(json.dumps(result, indent=2, ensure_ascii=True, default=str))
@@ -108,9 +97,6 @@ def write_note(
10897
typer.echo("Empty content provided. Please provide non-empty content.", err=True)
10998
raise typer.Exit(1)
11099

111-
config_manager = ConfigManager()
112-
project_name = _resolve_project(config_manager, project)
113-
114100
assert content is not None
115101

116102
with force_routing(local=local, cloud=cloud):
@@ -119,7 +105,7 @@ def write_note(
119105
title=title,
120106
content=content,
121107
directory=folder,
122-
project=project_name,
108+
project=project,
123109
workspace=workspace,
124110
tags=tags,
125111
output_format="json",
@@ -168,14 +154,11 @@ def read_note(
168154
try:
169155
validate_routing_flags(local, cloud)
170156

171-
config_manager = ConfigManager()
172-
project_name = _resolve_project(config_manager, project)
173-
174157
with force_routing(local=local, cloud=cloud):
175158
result = run_with_cleanup(
176159
mcp_read_note(
177160
identifier=identifier,
178-
project=project_name,
161+
project=project,
179162
workspace=workspace,
180163
page=page,
181164
page_size=page_size,
@@ -237,16 +220,13 @@ def edit_note(
237220
try:
238221
validate_routing_flags(local, cloud)
239222

240-
config_manager = ConfigManager()
241-
project_name = _resolve_project(config_manager, project)
242-
243223
with force_routing(local=local, cloud=cloud):
244224
result = run_with_cleanup(
245225
mcp_edit_note(
246226
identifier=identifier,
247227
operation=operation,
248228
content=content,
249-
project=project_name,
229+
project=project,
250230
workspace=workspace,
251231
section=section,
252232
find_text=find_text,
@@ -304,14 +284,11 @@ def build_context(
304284
try:
305285
validate_routing_flags(local, cloud)
306286

307-
config_manager = ConfigManager()
308-
project_name = _resolve_project(config_manager, project)
309-
310287
with force_routing(local=local, cloud=cloud):
311288
result = run_with_cleanup(
312289
mcp_build_context(
313290
url=url,
314-
project=project_name,
291+
project=project,
315292
workspace=workspace,
316293
depth=depth,
317294
timeframe=timeframe,
@@ -365,9 +342,6 @@ def recent_activity(
365342
try:
366343
validate_routing_flags(local, cloud)
367344

368-
config_manager = ConfigManager()
369-
project_name = _resolve_project(config_manager, project)
370-
371345
with force_routing(local=local, cloud=cloud):
372346
result = run_with_cleanup(
373347
mcp_recent_activity(
@@ -376,7 +350,7 @@ def recent_activity(
376350
timeframe=timeframe if timeframe is not None else "7d",
377351
page=page,
378352
page_size=page_size,
379-
project=project_name,
353+
project=project,
380354
workspace=workspace,
381355
output_format="json",
382356
)
@@ -460,9 +434,6 @@ def search_notes(
460434
try:
461435
validate_routing_flags(local, cloud)
462436

463-
config_manager = ConfigManager()
464-
project_name = _resolve_project(config_manager, project)
465-
466437
mode_flags = [permalink, title, vector, hybrid]
467438
if sum(1 for enabled in mode_flags if enabled) > 1: # pragma: no cover
468439
typer.echo(
@@ -515,7 +486,7 @@ def search_notes(
515486
result = run_with_cleanup(
516487
mcp_search(
517488
query=query or "",
518-
project=project_name,
489+
project=project,
519490
workspace=workspace,
520491
search_type=search_type,
521492
output_format="json",
@@ -649,9 +620,6 @@ def schema_validate(
649620
try:
650621
validate_routing_flags(local, cloud)
651622

652-
config_manager = ConfigManager()
653-
project_name = _resolve_project(config_manager, project)
654-
655623
# Heuristic: if target contains / or ., treat as identifier; otherwise as note type
656624
note_type, identifier = None, None
657625
if target:
@@ -665,7 +633,7 @@ def schema_validate(
665633
mcp_schema_validate(
666634
note_type=note_type,
667635
identifier=identifier,
668-
project=project_name,
636+
project=project,
669637
workspace=workspace,
670638
output_format="json",
671639
)
@@ -717,15 +685,12 @@ def schema_infer(
717685
try:
718686
validate_routing_flags(local, cloud)
719687

720-
config_manager = ConfigManager()
721-
project_name = _resolve_project(config_manager, project)
722-
723688
with force_routing(local=local, cloud=cloud):
724689
result = run_with_cleanup(
725690
mcp_schema_infer(
726691
note_type=entity_type,
727692
threshold=threshold,
728-
project=project_name,
693+
project=project,
729694
workspace=workspace,
730695
output_format="json",
731696
)
@@ -773,14 +738,11 @@ def schema_diff(
773738
try:
774739
validate_routing_flags(local, cloud)
775740

776-
config_manager = ConfigManager()
777-
project_name = _resolve_project(config_manager, project)
778-
779741
with force_routing(local=local, cloud=cloud):
780742
result = run_with_cleanup(
781743
mcp_schema_diff(
782744
note_type=entity_type,
783-
project=project_name,
745+
project=project,
784746
workspace=workspace,
785747
output_format="json",
786748
)

src/basic_memory/config.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,10 +425,12 @@ def is_test_env(self) -> bool:
425425
def get_project_mode(self, project_name: str) -> ProjectMode:
426426
"""Get the routing mode for a project.
427427
428-
Returns the per-project mode if set, otherwise LOCAL.
428+
Returns the per-project mode if set.
429+
Unknown projects (not in local config) default to CLOUD —
430+
local projects are always registered in config.
429431
"""
430432
entry = self.projects.get(project_name)
431-
return entry.mode if entry else ProjectMode.LOCAL
433+
return entry.mode if entry else ProjectMode.CLOUD
432434

433435
def set_project_mode(self, project_name: str, mode: ProjectMode) -> None:
434436
"""Set the routing mode for a project.

src/basic_memory/mcp/project_context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ async def get_project_client(
445445

446446
# Step 3: Determine if cloud routing is needed
447447
config = ConfigManager().config
448+
project_entry = config.projects.get(resolved_project)
448449
project_mode = config.get_project_mode(resolved_project)
449450

450451
# Trigger: workspace provided for a local project (without explicit --cloud)
@@ -459,7 +460,6 @@ async def get_project_client(
459460
if project_mode == ProjectMode.CLOUD or (_explicit_routing() and not _force_local_mode()):
460461
# --- Cloud routing: resolve workspace with priority chain ---
461462
effective_workspace = workspace
462-
project_entry = config.projects.get(resolved_project)
463463

464464
# Priority 2: per-project workspace_id from config
465465
if effective_workspace is None and project_entry and project_entry.workspace_id:

0 commit comments

Comments
 (0)