mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
2e5813d31e
Refactor CLI commands to use typed ProjectClient instead of raw HTTP calls, and add workspace metadata to cloud project listings so users can distinguish personal vs organization projects. Key changes: - 🔧 CLI commands now use ProjectClient typed API clients instead of call_get/call_post with manual URL construction - 🏢 Cloud project listings include workspace_name, workspace_type, and workspace_tenant_id for each cloud-sourced project - Pass config.default_workspace when fetching cloud projects via _fetch_cloud_projects() and CLI list_projects - Add --workspace flag to `bm project list` for explicit workspace override - Add "Workspace" column to CLI project list table - Add `bm tool list-projects` and `bm tool list-workspaces` JSON commands - Comprehensive tests for workspace passthrough, merge behavior, and CLI routing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
110 lines
3.7 KiB
Python
110 lines
3.7 KiB
Python
"""utility functions for commands"""
|
|
|
|
import asyncio
|
|
from typing import Optional, TypeVar, Coroutine, Any
|
|
|
|
from mcp.server.fastmcp.exceptions import ToolError
|
|
import typer
|
|
|
|
from rich.console import Console
|
|
|
|
from basic_memory import db
|
|
from basic_memory.config import ConfigManager
|
|
from basic_memory.mcp.async_client import get_client
|
|
from basic_memory.mcp.clients import ProjectClient
|
|
from basic_memory.mcp.project_context import get_active_project
|
|
|
|
console = Console()
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
def run_with_cleanup(coro: Coroutine[Any, Any, T]) -> T:
|
|
"""Run an async coroutine with proper database cleanup.
|
|
|
|
This helper ensures database connections are cleaned up before the
|
|
event loop closes, preventing process hangs in CLI commands.
|
|
|
|
Args:
|
|
coro: The coroutine to run
|
|
|
|
Returns:
|
|
The result of the coroutine
|
|
"""
|
|
|
|
async def _with_cleanup() -> T:
|
|
try:
|
|
return await coro
|
|
finally:
|
|
await db.shutdown_db()
|
|
|
|
return asyncio.run(_with_cleanup())
|
|
|
|
|
|
async def run_sync(
|
|
project: Optional[str] = None,
|
|
force_full: bool = False,
|
|
run_in_background: bool = True,
|
|
):
|
|
"""Run sync operation via API endpoint.
|
|
|
|
Args:
|
|
project: Optional project name
|
|
force_full: If True, force a full scan bypassing watermark optimization
|
|
run_in_background: If True, return immediately; if False, wait for completion
|
|
"""
|
|
|
|
# Resolve default project so get_client() can route per-project
|
|
project = project or ConfigManager().default_project
|
|
|
|
try:
|
|
async with get_client(project_name=project) as client:
|
|
project_item = await get_active_project(client, project, None)
|
|
project_client = ProjectClient(client)
|
|
data = await project_client.sync(
|
|
project_item.external_id,
|
|
force_full=force_full,
|
|
run_in_background=run_in_background,
|
|
)
|
|
# Background mode returns {"message": "..."}, foreground returns SyncReportResponse
|
|
if "message" in data:
|
|
console.print(f"[green]{data['message']}[/green]")
|
|
else:
|
|
# Foreground mode - show summary of sync results
|
|
total = data.get("total", 0)
|
|
new_count = len(data.get("new", []))
|
|
modified_count = len(data.get("modified", []))
|
|
deleted_count = len(data.get("deleted", []))
|
|
console.print(
|
|
f"[green]Synced {total} files[/green] "
|
|
f"(new: {new_count}, modified: {modified_count}, deleted: {deleted_count})"
|
|
)
|
|
except (ToolError, ValueError) as e:
|
|
console.print(f"[red]Sync failed: {e}[/red]")
|
|
raise typer.Exit(1)
|
|
|
|
|
|
async def get_project_info(project: str):
|
|
"""Get project information via API endpoint."""
|
|
try:
|
|
async with get_client(project_name=project) as client:
|
|
project_item = await get_active_project(client, project, None)
|
|
return await ProjectClient(client).get_info(project_item.external_id)
|
|
except (ToolError, ValueError) as e:
|
|
error_text = str(e)
|
|
if "internal proxy error" in error_text.lower() and "not found in configuration" in (
|
|
error_text.lower()
|
|
):
|
|
console.print(
|
|
"[red]Project info failed: cloud returned an internal configuration error for "
|
|
"this project.[/red]"
|
|
)
|
|
console.print(
|
|
"[yellow]This is a cloud backend issue for detailed info lookups. "
|
|
"Use `bm project list --cloud` for project metadata until the service is updated."
|
|
"[/yellow]"
|
|
)
|
|
else:
|
|
console.print(f"[red]Project info failed: {e}[/red]")
|
|
raise typer.Exit(1)
|