Skip to content

Commit d48b1dc

Browse files
committed
refactor project sync status info commands
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent c7da3bd commit d48b1dc

File tree

18 files changed

+346
-446
lines changed

18 files changed

+346
-446
lines changed

src/basic_memory/api/routers/project_router.py

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
"""Router for project management."""
22

33
import os
4-
from fastapi import APIRouter, HTTPException, Path, Body
4+
from fastapi import APIRouter, HTTPException, Path, Body, BackgroundTasks
55
from typing import Optional
6+
from loguru import logger
67

7-
from basic_memory.deps import ProjectServiceDep, ProjectPathDep
8-
from basic_memory.schemas import ProjectInfoResponse
8+
from basic_memory.deps import (
9+
ProjectConfigDep,
10+
ProjectServiceDep,
11+
ProjectPathDep,
12+
SyncServiceDep,
13+
)
14+
from basic_memory.schemas import ProjectInfoResponse, SyncReportResponse
915
from basic_memory.schemas.project_info import (
1016
ProjectList,
1117
ProjectItem,
@@ -97,6 +103,54 @@ async def update_project(
97103
raise HTTPException(status_code=400, detail=str(e))
98104

99105

106+
# Sync project filesystem
107+
@project_router.post("/sync")
108+
async def sync_project(
109+
background_tasks: BackgroundTasks,
110+
sync_service: SyncServiceDep,
111+
project_config: ProjectConfigDep,
112+
):
113+
"""Force project filesystem sync to database.
114+
115+
Scans the project directory and updates the database with any new or modified files.
116+
117+
Args:
118+
background_tasks: FastAPI background tasks
119+
sync_service: Sync service for this project
120+
project_config: Project configuration
121+
122+
Returns:
123+
Response confirming sync was initiated
124+
"""
125+
background_tasks.add_task(sync_service.sync, project_config.home, project_config.name)
126+
logger.info(f"Filesystem sync initiated for project: {project_config.name}")
127+
128+
return {
129+
"status": "sync_started",
130+
"message": f"Filesystem sync initiated for project '{project_config.name}'",
131+
}
132+
133+
134+
@project_router.post("/status", response_model=SyncReportResponse)
135+
async def project_sync_status(
136+
sync_service: SyncServiceDep,
137+
project_config: ProjectConfigDep,
138+
) -> SyncReportResponse:
139+
"""Scan directory for changes compared to database state.
140+
141+
Args:
142+
sync_service: Sync service for this project
143+
project_config: Project configuration
144+
145+
Returns:
146+
Scan report with details on files that need syncing
147+
"""
148+
logger.info(f"Scanning filesystem for project: {project_config.name}")
149+
sync_report = await sync_service.scan(project_config.home)
150+
151+
return SyncReportResponse.from_sync_report(sync_report)
152+
153+
100154
# List all available projects
101155
@project_resource_router.get("/projects", response_model=ProjectList)
102156
async def list_projects(
@@ -259,7 +313,7 @@ async def get_default_project(
259313

260314

261315
# Synchronize projects between config and database
262-
@project_resource_router.post("/sync", response_model=ProjectStatusResponse)
316+
@project_resource_router.post("/config/sync", response_model=ProjectStatusResponse)
263317
async def synchronize_projects(
264318
project_service: ProjectServiceDep,
265319
) -> ProjectStatusResponse:

src/basic_memory/cli/commands/cloud/api_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ async def make_api_request(
5454
async with httpx.AsyncClient(timeout=timeout) as client:
5555
try:
5656
console.print(f"[dim]Making {method} request to {url}[/dim]")
57-
console.print(f"[dim]Headers: {dict(headers)}[/dim]")
57+
# console.print(f"[dim]Headers: {dict(headers)}[/dim]")
5858

5959
response = await client.request(method=method, url=url, headers=headers, json=json_data)
6060

6161
console.print(f"[dim]Response status: {response.status_code}[/dim]")
62-
console.print(f"[dim]Response headers: {dict(response.headers)}[/dim]")
62+
# console.print(f"[dim]Response headers: {dict(response.headers)}[/dim]")
6363

6464
response.raise_for_status()
6565
return response

src/basic_memory/cli/commands/cloud/bisync_commands.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def __init__(
7474
}
7575

7676

77-
async def get_tenant_info() -> dict:
77+
async def get_mount_info() -> dict:
7878
"""Get current tenant information from cloud API."""
7979
try:
8080
config_manager = ConfigManager()
@@ -352,9 +352,9 @@ def setup_cloud_bisync(sync_dir: Optional[str] = None) -> None:
352352
console.print("[blue]Step 1: Installing rclone...[/blue]")
353353
install_rclone()
354354

355-
# Step 2: Get tenant info
355+
# Step 2: Get mount info (for tenant_id, bucket)
356356
console.print("\n[blue]Step 2: Getting tenant information...[/blue]")
357-
tenant_info = asyncio.run(get_tenant_info())
357+
tenant_info = asyncio.run(get_mount_info())
358358

359359
tenant_id = tenant_info.get("tenant_id")
360360
bucket_name = tenant_info.get("bucket_name")
@@ -449,7 +449,7 @@ def run_bisync(
449449
try:
450450
# Get tenant info if not provided
451451
if not tenant_id or not bucket_name:
452-
tenant_info = asyncio.run(get_tenant_info())
452+
tenant_info = asyncio.run(get_mount_info())
453453
tenant_id = tenant_info.get("tenant_id")
454454
bucket_name = tenant_info.get("bucket_name")
455455

@@ -461,6 +461,7 @@ def run_bisync(
461461
local_path = get_bisync_directory()
462462

463463
# Validate bisync directory
464+
# can be same dir as mount
464465
validate_bisync_directory(local_path)
465466

466467
# Check if local path exists
@@ -504,7 +505,9 @@ def run_bisync(
504505
asyncio.run(create_cloud_project(project_name))
505506
console.print(f"[green] ✓ Created project: {project_name}[/green]")
506507
except BisyncError as e:
507-
console.print(f"[yellow] ⚠ Could not create {project_name}: {e}[/yellow]")
508+
console.print(
509+
f"[yellow] ⚠ Could not create {project_name}: {e}[/yellow]"
510+
)
508511
else:
509512
console.print("[dim]All local projects already registered on cloud[/dim]")
510513

@@ -566,18 +569,32 @@ def run_bisync(
566569

567570

568571
async def notify_container_sync(tenant_id: str) -> None:
569-
"""Notify the cloud container to refresh its TigrisFS cache."""
572+
"""Sync all projects after bisync completes."""
570573
try:
571-
config_manager = ConfigManager()
572-
config = config_manager.config
573-
host_url = config.cloud_host.rstrip("/")
574+
from basic_memory.cli.commands.command_utils import run_sync
574575

575-
# Call the sync endpoint to trigger cache refresh
576-
await make_api_request(method="POST", url=f"{host_url}/proxy/sync")
577-
console.print("[dim]✓ Notified container to refresh cache[/dim]")
578-
except Exception:
579-
# Non-critical, don't fail the sync
580-
pass
576+
# Fetch all projects and sync each one
577+
cloud_data = await fetch_cloud_projects()
578+
projects = cloud_data.get("projects", [])
579+
580+
if not projects:
581+
console.print("[dim]No projects to sync[/dim]")
582+
return
583+
584+
console.print(f"[dim]Syncing {len(projects)} project(s)...[/dim]")
585+
586+
for project in projects:
587+
project_name = project.get("name")
588+
if project_name:
589+
try:
590+
await run_sync(project=project_name)
591+
except Exception as e:
592+
# Non-critical, log and continue
593+
console.print(f"[yellow] ⚠ Sync failed for {project_name}: {e}[/yellow]")
594+
595+
except Exception as e:
596+
# Non-critical, don't fail the bisync
597+
console.print(f"[yellow]Warning: Post-sync failed: {e}[/yellow]")
581598

582599

583600
def run_bisync_watch(
@@ -625,7 +642,7 @@ def show_bisync_status() -> None:
625642

626643
try:
627644
# Get tenant info
628-
tenant_info = asyncio.run(get_tenant_info())
645+
tenant_info = asyncio.run(get_mount_info())
629646
tenant_id = tenant_info.get("tenant_id")
630647

631648
if not tenant_id:
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""utility functions for commands"""
2+
3+
from typing import Optional
4+
5+
from mcp.server.fastmcp.exceptions import ToolError
6+
import typer
7+
from rich.console import Console
8+
from basic_memory.mcp.async_client import client
9+
10+
from basic_memory.mcp.tools.utils import call_post
11+
from basic_memory.mcp.project_context import get_active_project
12+
13+
console = Console()
14+
15+
16+
async def run_sync(project: Optional[str] = None):
17+
"""Run sync operation via API endpoint."""
18+
19+
try:
20+
project_item = await get_active_project(client, project, None)
21+
response = await call_post(client, f"{project_item.project_url}/project/sync")
22+
data = response.json()
23+
console.print(f"[green]✓ {data['message']}[/green]")
24+
except (ToolError, ValueError) as e:
25+
console.print(f"[red]✗ Sync failed: {e}[/red]")
26+
raise typer.Exit(1)

src/basic_memory/cli/commands/project.py

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from datetime import datetime
1515

1616
from rich.panel import Panel
17-
from rich.tree import Tree
1817
from basic_memory.mcp.async_client import client
1918
from basic_memory.mcp.tools.utils import call_get
2019
from basic_memory.schemas.project_info import ProjectList
@@ -125,19 +124,14 @@ def set_default_project(
125124
console.print(f"[red]Error setting default project: {str(e)}[/red]")
126125
raise typer.Exit(1)
127126

128-
# The API call above updates the config file default
129-
console.print(
130-
f"[green]CLI commands will now use '{name}' when no --project flag is specified[/green]"
131-
)
132-
133127

134128
@project_app.command("sync-config")
135129
def synchronize_projects() -> None:
136130
"""Synchronize project config between configuration file and database."""
137131
# Call the API to synchronize projects
138132

139133
try:
140-
response = asyncio.run(call_post(client, "/projects/sync"))
134+
response = asyncio.run(call_post(client, "/projects/config/sync"))
141135
result = ProjectStatusResponse.model_validate(response.json())
142136

143137
console.print(f"[green]{result.message}[/green]")
@@ -189,12 +183,13 @@ def move_project(
189183

190184
@project_app.command("info")
191185
def display_project_info(
186+
name: str = typer.Argument(..., help="Name of the project"),
192187
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
193188
):
194189
"""Display detailed information and statistics about the current project."""
195190
try:
196191
# Get project info
197-
info = asyncio.run(project_info.fn()) # type: ignore # pyright: ignore [reportAttributeAccessIssue]
192+
info = asyncio.run(project_info.fn(name)) # type: ignore # pyright: ignore [reportAttributeAccessIssue]
198193

199194
if json_output:
200195
# Convert to JSON and print
@@ -206,6 +201,7 @@ def display_project_info(
206201
# Project configuration section
207202
console.print(
208203
Panel(
204+
f"Basic Memory version: [bold green]{info.system.version}[/bold green]\n"
209205
f"[bold]Project:[/bold] {info.project_name}\n"
210206
f"[bold]Path:[/bold] {info.project_path}\n"
211207
f"[bold]Default Project:[/bold] {info.default_project}\n",
@@ -275,42 +271,6 @@ def display_project_info(
275271

276272
console.print(recent_table)
277273

278-
# System status
279-
system_tree = Tree("🖥️ System Status")
280-
system_tree.add(f"Basic Memory version: [bold green]{info.system.version}[/bold green]")
281-
system_tree.add(
282-
f"Database: [cyan]{info.system.database_path}[/cyan] ([green]{info.system.database_size}[/green])"
283-
)
284-
285-
# Watch status
286-
if info.system.watch_status: # pragma: no cover
287-
watch_branch = system_tree.add("Watch Service")
288-
running = info.system.watch_status.get("running", False)
289-
status_color = "green" if running else "red"
290-
watch_branch.add(
291-
f"Status: [bold {status_color}]{'Running' if running else 'Stopped'}[/bold {status_color}]"
292-
)
293-
294-
if running:
295-
start_time = (
296-
datetime.fromisoformat(info.system.watch_status.get("start_time", ""))
297-
if isinstance(info.system.watch_status.get("start_time"), str)
298-
else info.system.watch_status.get("start_time")
299-
)
300-
watch_branch.add(
301-
f"Running since: [cyan]{start_time.strftime('%Y-%m-%d %H:%M')}[/cyan]"
302-
)
303-
watch_branch.add(
304-
f"Files synced: [green]{info.system.watch_status.get('synced_files', 0)}[/green]"
305-
)
306-
watch_branch.add(
307-
f"Errors: [{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]{info.system.watch_status.get('error_count', 0)}[/{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]"
308-
)
309-
else:
310-
system_tree.add("[yellow]Watch service not running[/yellow]")
311-
312-
console.print(system_tree)
313-
314274
# Available projects
315275
projects_table = Table(title="📁 Available Projects")
316276
projects_table.add_column("Name", style="blue")

0 commit comments

Comments
 (0)