mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user