Skip to main content
Glama

basic-memory

SPEC-20 Simplified Project-Scoped Rclone Sync.md38.8 kB
--- title: 'SPEC-20: Simplified Project-Scoped Rclone Sync' date: 2025-01-27 updated: 2025-01-28 status: Implemented priority: High goal: Simplify cloud sync by making it project-scoped, safe by design, and closer to native rclone commands parent: SPEC-8 --- ## Executive Summary The current rclone implementation (SPEC-8) has proven too complex with multiple footguns: - Two workflows (mount vs bisync) with different directories causing confusion - Multiple profiles (3 for mount, 3 for bisync) creating too much choice - Directory conflicts (`~/basic-memory-cloud/` vs `~/basic-memory-cloud-sync/`) - Auto-discovery of folders leading to errors - Unclear what syncs and when This spec proposes a **radical simplification**: project-scoped sync operations that are explicit, safe, and thin wrappers around rclone commands. ## Why ### Current Problems **Complexity:** - Users must choose between mount and bisync workflows - Different directories for different workflows - Profile selection (6 total profiles) overwhelms users - Setup requires multiple steps with potential conflicts **Footguns:** - Renaming local folder breaks sync (no config tracking) - Mount directory conflicts with bisync directory - Auto-discovered folders create phantom projects - Uninitialized bisync state causes failures - Unclear which files sync (all files in root directory?) **User Confusion:** - "What does `bm sync` actually do?" - "Is `~/basic-memory-cloud-sync/my-folder/` a project or just a folder?" - "Why do I have two basic-memory directories?" - "How do I sync just one project?" ### Design Principles (Revised) 1. **Projects are independent** - Each project manages its own sync state 2. **Global cloud mode** - You're either local or cloud (no per-project flag) 3. **Explicit operations** - No auto-discovery, no magic 4. **Safe by design** - Config tracks state, not filesystem 5. **Thin rclone wrappers** - Stay close to rclone commands 6. **One good way** - Remove choices that don't matter ## What ### New Architecture #### Core Concepts 1. **Global Cloud Mode** (existing, keep as-is) - `cloud_mode_enabled` in config - `bm cloud login` enables it, `bm cloud logout` disables it - When enabled, ALL Basic Memory operations hit cloud API 2. **Project-Scoped Sync** (new) - Each project optionally has a `local_path` for local working copy - Sync operations are explicit: `bm project sync --name research` - Projects can live anywhere on disk, not forced into sync directory 3. **Simplified rclone Config** (new) - Single remote named `bm-cloud` (not `basic-memory-{tenant_id}`) - One credential set per user - Config lives at `~/.config/rclone/rclone.conf` #### Command Structure ```bash # Setup (once) bm cloud login # Authenticate, enable cloud mode bm cloud setup # Install rclone, generate credentials # Project creation with optional sync bm project add research # Create cloud project (no local sync) bm project add research --local ~/docs # Create with local sync configured # Or configure sync later bm project sync-setup research ~/docs # Configure local sync for existing project # Project-scoped rclone operations bm project sync --name research # One-way sync (local → cloud) bm project bisync --name research # Two-way sync (local ↔ cloud) bm project check --name research # Verify integrity # Advanced file operations bm project ls --name research [path] # List remote files bm project copy --name research src dst # Copy files # Batch operations bm project sync --all # Sync all projects with local_sync_path bm project bisync --all # Two-way sync all projects ``` #### Config Model ```json { "cloud_mode": true, "cloud_host": "https://cloud.basicmemory.com", "projects": { // Used in LOCAL mode only (simple name → path mapping) "main": "/Users/user/basic-memory" }, "cloud_projects": { // Used in CLOUD mode for sync configuration "research": { "local_path": "~/Documents/research", "last_sync": "2025-01-27T10:00:00Z", "bisync_initialized": true }, "work": { "local_path": "~/work", "last_sync": null, "bisync_initialized": false } } } ``` **Note:** In cloud mode, the actual project list comes from the API (`GET /projects/projects`). The `cloud_projects` dict only stores local sync configuration. #### Rclone Config ```ini # ~/.config/rclone/rclone.conf [basic-memory-cloud] type = s3 provider = Other access_key_id = {scoped_access_key} secret_access_key = {scoped_secret_key} endpoint = https://fly.storage.tigris.dev region = auto ``` ### What Gets Removed - ❌ Mount commands (`bm cloud mount`, `bm cloud unmount`, `bm cloud mount-status`) - ❌ Profile selection (both mount and bisync profiles) - ❌ `~/basic-memory-cloud/` directory (mount point) - ❌ `~/basic-memory-cloud-sync/` directory (forced sync location) - ❌ Auto-discovery of folders - ❌ Separate `bisync-setup` command - ❌ `bisync_config` in config.json - ❌ Convenience commands (`bm sync`, `bm bisync` without project name) - ❌ Complex state management for global sync ### What Gets Simplified - ✅ One setup command: `bm cloud setup` - ✅ One rclone remote: `bm-cloud` - ✅ One workflow: project-scoped bisync (remove mount) - ✅ One set of defaults (balanced settings from SPEC-8) - ✅ Clear project-to-path mapping in config - ✅ Explicit sync operations only ### What Gets Added - ✅ `bm project sync --name <project>` (one-way: local → cloud) - ✅ `bm project bisync --name <project>` (two-way: local ↔ cloud) - ✅ `bm project check --name <project>` (integrity verification) - ✅ `bm project sync-setup <project> <local_path>` (configure sync) - ✅ `bm project ls --name <project> [path]` (list remote files) - ✅ `bm project copy --name <project> <src> <dst>` (copy files) - ✅ `cloud_projects` dict in config.json ## How ### Phase 1: Project Model Updates **1.1 Update Config Schema** ```python # basic_memory/config.py class Config(BaseModel): # ... existing fields ... cloud_mode: bool = False cloud_host: str = "https://cloud.basicmemory.com" # Local mode: simple name → path mapping projects: dict[str, str] = {} # Cloud mode: sync configuration per project cloud_projects: dict[str, CloudProjectConfig] = {} class CloudProjectConfig(BaseModel): """Sync configuration for a cloud project.""" local_path: str # Local working directory last_sync: Optional[datetime] = None # Last successful sync bisync_initialized: bool = False # Whether bisync baseline exists ``` **No database changes needed** - sync config lives in `~/.basic-memory/config.json` only. ### Phase 2: Simplified Rclone Configuration **2.1 Simplify Remote Naming** ```python # basic_memory/cli/commands/cloud/rclone_config.py def configure_rclone_remote( access_key: str, secret_key: str, endpoint: str = "https://fly.storage.tigris.dev", region: str = "auto", ) -> None: """Configure single rclone remote named 'bm-cloud'.""" config = load_rclone_config() # Single remote name (not tenant-specific) REMOTE_NAME = "basic-memory-cloud" if not config.has_section(REMOTE_NAME): config.add_section(REMOTE_NAME) config.set(REMOTE_NAME, "type", "s3") config.set(REMOTE_NAME, "provider", "Other") config.set(REMOTE_NAME, "access_key_id", access_key) config.set(REMOTE_NAME, "secret_access_key", secret_key) config.set(REMOTE_NAME, "endpoint", endpoint) config.set(REMOTE_NAME, "region", region) save_rclone_config(config) ``` **2.2 Remove Profile Complexity** Use single set of balanced defaults (from SPEC-8 Phase 4 testing): - `conflict_resolve`: `newer` (auto-resolve to most recent) - `max_delete`: `25` (safety limit) - `check_access`: `false` (skip for performance) ### Phase 3: Project-Scoped Rclone Commands **3.1 Core Sync Operations** ```python # basic_memory/cli/commands/cloud/rclone_commands.py def get_project_remote(project: Project, bucket_name: str) -> str: """Build rclone remote path for project. Returns: bm-cloud:bucket-name/app/data/research """ # Strip leading slash from cloud path cloud_path = project.path.lstrip("/") return f"basic-memory-cloud:{bucket_name}/{cloud_path}" def project_sync( project: Project, bucket_name: str, dry_run: bool = False, verbose: bool = False, ) -> bool: """One-way sync: local → cloud. Uses rclone sync to make cloud identical to local. """ if not project.local_sync_path: raise RcloneError(f"Project {project.name} has no local_sync_path configured") local_path = Path(project.local_sync_path).expanduser() remote_path = get_project_remote(project, bucket_name) filter_path = get_bmignore_filter_path() cmd = [ "rclone", "sync", str(local_path), remote_path, "--filters-file", str(filter_path), ] if verbose: cmd.append("--verbose") else: cmd.append("--progress") if dry_run: cmd.append("--dry-run") result = subprocess.run(cmd, text=True) return result.returncode == 0 def project_bisync( project: Project, bucket_name: str, dry_run: bool = False, resync: bool = False, verbose: bool = False, ) -> bool: """Two-way sync: local ↔ cloud. Uses rclone bisync with balanced defaults. """ if not project.local_sync_path: raise RcloneError(f"Project {project.name} has no local_sync_path configured") local_path = Path(project.local_sync_path).expanduser() remote_path = get_project_remote(project, bucket_name) filter_path = get_bmignore_filter_path() state_path = get_project_bisync_state(project.name) # Ensure state directory exists state_path.mkdir(parents=True, exist_ok=True) cmd = [ "rclone", "bisync", str(local_path), remote_path, "--create-empty-src-dirs", "--resilient", "--conflict-resolve=newer", "--max-delete=25", "--filters-file", str(filter_path), "--workdir", str(state_path), ] if verbose: cmd.append("--verbose") else: cmd.append("--progress") if dry_run: cmd.append("--dry-run") if resync: cmd.append("--resync") # Check if first run requires resync if not resync and not bisync_initialized(project.name) and not dry_run: raise RcloneError( f"First bisync for {project.name} requires --resync to establish baseline.\n" f"Run: bm project bisync --name {project.name} --resync" ) result = subprocess.run(cmd, text=True) return result.returncode == 0 def project_check( project: Project, bucket_name: str, one_way: bool = False, ) -> bool: """Check integrity between local and cloud. Returns True if files match, False if differences found. """ if not project.local_sync_path: raise RcloneError(f"Project {project.name} has no local_sync_path configured") local_path = Path(project.local_sync_path).expanduser() remote_path = get_project_remote(project, bucket_name) filter_path = get_bmignore_filter_path() cmd = [ "rclone", "check", str(local_path), remote_path, "--filter-from", str(filter_path), ] if one_way: cmd.append("--one-way") result = subprocess.run(cmd, capture_output=True, text=True) return result.returncode == 0 ``` **3.2 Advanced File Operations** ```python def project_ls( project: Project, bucket_name: str, path: Optional[str] = None, ) -> list[str]: """List files in remote project.""" remote_path = get_project_remote(project, bucket_name) if path: remote_path = f"{remote_path}/{path}" cmd = ["rclone", "ls", remote_path] result = subprocess.run(cmd, capture_output=True, text=True, check=True) return result.stdout.splitlines() def project_copy( project: Project, bucket_name: str, src: str, dst: str, dry_run: bool = False, ) -> bool: """Copy files within project scope.""" # Implementation similar to sync pass ``` ### Phase 4: CLI Integration **4.1 Update Project Commands** ```python # basic_memory/cli/commands/project.py @project_app.command("add") def add_project( name: str = typer.Argument(..., help="Name of the project"), path: str = typer.Argument(None, help="Path (required for local mode)"), local: str = typer.Option(None, "--local", help="Local sync path for cloud mode"), set_default: bool = typer.Option(False, "--default", help="Set as default"), ) -> None: """Add a new project. Cloud mode examples: bm project add research # No local sync bm project add research --local ~/docs # With local sync Local mode example: bm project add research ~/Documents/research """ config = ConfigManager().config if config.cloud_mode_enabled: # Cloud mode: auto-generate cloud path from name async def _add_project(): async with get_client() as client: data = { "name": name, "path": generate_permalink(name), "local_sync_path": local, # Optional "set_default": set_default, } response = await call_post(client, "/projects/projects", json=data) return ProjectStatusResponse.model_validate(response.json()) else: # Local mode: path is required if path is None: console.print("[red]Error: path argument is required in local mode[/red]") raise typer.Exit(1) resolved_path = Path(os.path.abspath(os.path.expanduser(path))).as_posix() async def _add_project(): async with get_client() as client: data = { "name": name, "path": resolved_path, "set_default": set_default, } response = await call_post(client, "/projects/projects", json=data) return ProjectStatusResponse.model_validate(response.json()) # Execute and display result result = asyncio.run(_add_project()) console.print(f"[green]{result.message}[/green]") @project_app.command("sync-setup") def setup_project_sync( name: str = typer.Argument(..., help="Project name"), local_path: str = typer.Argument(..., help="Local sync directory"), ) -> None: """Configure local sync for an existing cloud project.""" config = ConfigManager().config if not config.cloud_mode_enabled: console.print("[red]Error: sync-setup only available in cloud mode[/red]") raise typer.Exit(1) resolved_path = Path(os.path.abspath(os.path.expanduser(local_path))).as_posix() async def _update_project(): async with get_client() as client: data = {"local_sync_path": resolved_path} project_permalink = generate_permalink(name) response = await call_patch( client, f"/projects/{project_permalink}", json=data, ) return ProjectStatusResponse.model_validate(response.json()) result = asyncio.run(_update_project()) console.print(f"[green]{result.message}[/green]") console.print(f"\nLocal sync configured: {resolved_path}") console.print(f"\nTo sync: bm project bisync --name {name} --resync") ``` **4.2 New Sync Commands** ```python # basic_memory/cli/commands/project.py @project_app.command("sync") def sync_project( name: str = typer.Option(..., "--name", help="Project name"), all_projects: bool = typer.Option(False, "--all", help="Sync all projects"), dry_run: bool = typer.Option(False, "--dry-run", help="Preview changes"), verbose: bool = typer.Option(False, "--verbose", help="Show detailed output"), ) -> None: """One-way sync: local → cloud (make cloud identical to local).""" config = ConfigManager().config if not config.cloud_mode_enabled: console.print("[red]Error: sync only available in cloud mode[/red]") raise typer.Exit(1) # Get projects to sync if all_projects: projects = get_all_sync_projects() else: projects = [get_project_by_name(name)] # Get bucket name tenant_info = asyncio.run(get_mount_info()) bucket_name = tenant_info.bucket_name # Sync each project for project in projects: if not project.local_sync_path: console.print(f"[yellow]Skipping {project.name}: no local_sync_path[/yellow]") continue console.print(f"[blue]Syncing {project.name}...[/blue]") try: project_sync(project, bucket_name, dry_run=dry_run, verbose=verbose) console.print(f"[green]✓ {project.name} synced[/green]") except RcloneError as e: console.print(f"[red]✗ {project.name} failed: {e}[/red]") @project_app.command("bisync") def bisync_project( name: str = typer.Option(..., "--name", help="Project name"), all_projects: bool = typer.Option(False, "--all", help="Bisync all projects"), dry_run: bool = typer.Option(False, "--dry-run", help="Preview changes"), resync: bool = typer.Option(False, "--resync", help="Force new baseline"), verbose: bool = typer.Option(False, "--verbose", help="Show detailed output"), ) -> None: """Two-way sync: local ↔ cloud (bidirectional sync).""" # Similar to sync but calls project_bisync() pass @project_app.command("check") def check_project( name: str = typer.Option(..., "--name", help="Project name"), one_way: bool = typer.Option(False, "--one-way", help="Check one direction only"), ) -> None: """Verify file integrity between local and cloud.""" # Calls project_check() pass @project_app.command("ls") def list_project_files( name: str = typer.Option(..., "--name", help="Project name"), path: str = typer.Argument(None, help="Path within project"), ) -> None: """List files in remote project.""" # Calls project_ls() pass ``` **4.3 Update Cloud Setup** ```python # basic_memory/cli/commands/cloud/core_commands.py @cloud_app.command("setup") def cloud_setup() -> None: """Set up cloud sync (install rclone and configure credentials).""" console.print("[bold blue]Basic Memory Cloud Setup[/bold blue]") console.print("Installing rclone and configuring credentials...\n") try: # Step 1: Install rclone console.print("[blue]Step 1: Installing rclone...[/blue]") install_rclone() # Step 2: Get tenant info console.print("\n[blue]Step 2: Getting tenant information...[/blue]") tenant_info = asyncio.run(get_mount_info()) tenant_id = tenant_info.tenant_id console.print(f"[green]✓ Tenant: {tenant_id}[/green]") # Step 3: Generate credentials console.print("\n[blue]Step 3: Generating sync credentials...[/blue]") creds = asyncio.run(generate_mount_credentials(tenant_id)) console.print("[green]✓ Generated credentials[/green]") # Step 4: Configure rclone (single remote: bm-cloud) console.print("\n[blue]Step 4: Configuring rclone...[/blue]") configure_rclone_remote( access_key=creds.access_key, secret_key=creds.secret_key, ) console.print("\n[bold green]✓ Cloud setup completed![/bold green]") console.print("\nNext steps:") console.print(" 1. Create projects with local sync:") console.print(" bm project add research --local ~/Documents/research") console.print("\n 2. Or configure sync for existing projects:") console.print(" bm project sync-setup research ~/Documents/research") console.print("\n 3. Start syncing:") console.print(" bm project bisync --name research --resync") except Exception as e: console.print(f"\n[red]Setup failed: {e}[/red]") raise typer.Exit(1) ``` ### Phase 5: Cleanup **5.1 Remove Deprecated Commands** ```python # Remove from cloud commands: - cloud mount - cloud unmount - cloud mount-status - bisync-setup - Individual bisync command (moved to project bisync) # Remove from root commands: - bm sync (without project specification) - bm bisync (without project specification) ``` **5.2 Remove Deprecated Code** ```python # Files to remove: - mount_commands.py (entire file) # Functions to remove from rclone_config.py: - MOUNT_PROFILES - get_default_mount_path() - build_mount_command() - is_path_mounted() - get_rclone_processes() - kill_rclone_process() - unmount_path() - cleanup_orphaned_rclone_processes() # Functions to remove from bisync_commands.py: - BISYNC_PROFILES (use single default) - setup_cloud_bisync() (replaced by cloud setup) - run_bisync_watch() (can add back to project bisync if needed) - show_bisync_status() (replaced by project list showing sync status) ``` **5.3 Update Configuration Schema** ```python # Remove from config.json: - bisync_config (no longer needed) # The projects array is the source of truth for sync configuration ``` ### Phase 6: Documentation Updates **6.1 Update CLI Documentation** ```markdown # docs/cloud-cli.md ## Project-Scoped Cloud Sync Basic Memory cloud sync is project-scoped - each project can optionally be configured with a local working directory that syncs with the cloud. ### Setup (One Time) 1. Authenticate and enable cloud mode: ```bash bm cloud login ``` 2. Install rclone and configure credentials: ```bash bm cloud setup ``` ### Create Projects with Sync Create a cloud project with optional local sync: ```bash # Create project without local sync bm project add research # Create project with local sync bm project add research --local ~/Documents/research ``` Or configure sync for existing (remote) project: ```bash bm project sync-setup research ~/Documents/research ``` ### Syncing Projects **Two-way sync (recommended):** ```bash # First time - establish baseline bm project bisync --name research --resync # Subsequent syncs bm project bisync --name research # Sync all projects with local_sync_path configured bm project bisync --all ``` **One-way sync (local → cloud):** ```bash bm project sync --name research ``` **Verify integrity:** ```bash bm project check --name research ``` ### Advanced Operations **List remote files:** ```bash bm project ls --name research bm project ls --name research subfolder ``` **Preview changes before syncing:** ```bash bm project bisync --name research --dry-run ``` **Verbose output for debugging:** ```bash bm project bisync --name research --verbose ``` ### Project Management **List projects (shows sync status):** ```bash bm project list ``` **Update sync path:** ```bash bm project sync-setup research ~/new/path ``` **Remove project:** ```bash bm project remove research ``` ``` **6.2 Update SPEC-8** Add to SPEC-8's "Implementation Notes" section: ```markdown ## Superseded by SPEC-20 The initial implementation in SPEC-8 proved too complex with multiple footguns: - Mount vs bisync workflow confusion - Multiple profiles creating decision paralysis - Directory conflicts and auto-discovery errors SPEC-20 supersedes the sync implementation with a simplified project-scoped approach while keeping the core Tigris infrastructure from SPEC-8. ``` ## How to Evaluate ### Success Criteria **1. Simplified Setup** - [ ] `bm cloud setup` completes in one command - [ ] Creates single rclone remote named `bm-cloud` - [ ] No profile selection required - [ ] Clear next steps printed after setup **2. Clear Project Model** - [ ] Projects can be created with or without local sync - [ ] `bm project list` shows sync status for each project - [ ] `local_sync_path` stored in project config - [ ] Renaming local folder doesn't break sync (config is source of truth) **3. Working Sync Operations** - [ ] `bm project sync --name <project>` performs one-way sync - [ ] `bm project bisync --name <project>` performs two-way sync - [ ] `bm project check --name <project>` verifies integrity - [ ] `--all` flag syncs all configured projects - [ ] `--dry-run` shows changes without applying - [ ] First bisync requires `--resync` with clear error message **4. Safety** - [ ] Cannot sync project without `local_sync_path` configured - [ ] Bisync state is per-project (not global) - [ ] `.bmignore` patterns respected - [ ] Max delete safety (25 files) prevents accidents - [ ] Clear error messages for all failure modes **5. Clean Removal** - [ ] Mount commands removed - [ ] Profile selection removed - [ ] Global sync directory removed (`~/basic-memory-cloud-sync/`) - [ ] Auto-discovery removed - [ ] Convenience commands (`bm sync`) removed **6. Documentation** - [ ] Updated cloud-cli.md with new workflow - [ ] Clear examples for common operations - [ ] Migration guide for existing users - [ ] Troubleshooting section ### Test Scenarios **Scenario 1: New User Setup** ```bash # Start fresh bm cloud login bm cloud setup bm project add research --local ~/docs/research bm project bisync --name research --resync # Edit files locally bm project bisync --name research # Verify changes synced ``` **Scenario 2: Multiple Projects** ```bash bm project add work --local ~/work bm project add personal --local ~/personal bm project bisync --all --resync # Edit files in both projects bm project bisync --all ``` **Scenario 3: Project Without Sync** ```bash bm project add temp-notes # Try to sync (should fail gracefully) bm project bisync --name temp-notes # Should see: "Project temp-notes has no local_sync_path configured" ``` **Scenario 4: Integrity Check** ```bash bm project bisync --name research # Manually edit file in cloud UI bm project check --name research # Should report differences bm project bisync --name research # Should sync changes back to local ``` **Scenario 5: Safety Features** ```bash # Delete 30 files locally bm project sync --name research # Should fail with max delete error # User reviews and confirms bm project sync --name research # After confirming ``` ### Performance Targets - Setup completes in < 30 seconds - Single project sync < 5 seconds for small changes - Bisync initialization (--resync) < 10 seconds for typical project - Batch sync (--all) processes N projects in N*5 seconds ### Breaking Changes This is a **breaking change** from SPEC-8 implementation: **Migration Required:** - Users must run `bm cloud setup` again - Existing `~/basic-memory-cloud-sync/` directory abandoned - Projects must be configured with `local_sync_path` - Mount users must switch to bisync workflow **Migration Guide:** ```bash # 1. Note current project locations bm project list # 2. Re-run setup bm cloud setup # 3. Configure sync for each project bm project sync-setup research ~/Documents/research bm project sync-setup work ~/work # 4. Establish baselines bm project bisync --all --resync # 5. Old directory can be deleted rm -rf ~/basic-memory-cloud-sync/ ``` ## Dependencies - **SPEC-8**: TigrisFS Integration (bucket provisioning, credentials) - Python 3.12+ - rclone 1.64.0+ - Typer (CLI framework) - Rich (console output) ## Risks **Risk 1: User Confusion from Breaking Changes** - Mitigation: Clear migration guide, version bump (0.16.0) - Mitigation: Detect old config and print migration instructions **Risk 2: Per-Project Bisync State Complexity** - Mitigation: Use rclone's `--workdir` to isolate state per project - Mitigation: Store in `~/.basic-memory/bisync-state/{project_name}/` **Risk 3: Batch Operations Performance** - Mitigation: Run syncs sequentially with progress indicators - Mitigation: Add `--parallel` flag in future if needed **Risk 4: Lost Features (Mount)** - Mitigation: Document mount as experimental/advanced feature - Mitigation: Can restore if users request it ## Open Questions 1. **Should we keep mount as experimental command?** - Lean toward: Remove entirely, focus on bisync - Alternative: Keep as `bm project mount --name <project>` (advanced) - Answer: remove 2. **Batch sync order?** - Alphabetical by project name? - By last modified time? - Let user specify order? - answer: project order from api or config 3. **Credential refresh?** - Auto-detect expired credentials and re-run credential generation? - Or require manual `bm cloud setup` again? - answer: manual setup is fine 4. **Watch mode for projects?** - `bm project bisync --name research --watch`? - Or removed entirely (users can use OS tools)? - answer: remove for now: we can add it back later if it's useful 5. **Project path validation?** - Ensure `local_path` exists before allowing bisync? - Or let rclone error naturally? - answer: create if needed, exists is ok ## Implementation Checklist ### Phase 1: Config Schema (1-2 days) ✅ - [x] Add `CloudProjectConfig` model to `basic_memory/config.py` - [x] Add `cloud_projects: dict[str, CloudProjectConfig]` to Config model - [x] Test config loading/saving with new schema - [x] Handle migration from old config format ### Phase 2: Rclone Config Simplification ✅ - [x] Update `configure_rclone_remote()` to use `basic-memory-cloud` as remote name - [x] Remove `add_tenant_to_rclone_config()` (replaced by configure_rclone_remote) - [x] Remove tenant_id from remote naming - [x] Test rclone config generation - [x] Clean up deprecated import references in bisync_commands.py and core_commands.py ### Phase 3: Project-Scoped Rclone Commands ✅ - [x] Create `src/basic_memory/cli/commands/cloud/rclone_commands.py` - [x] Implement `get_project_remote(project, bucket_name)` - [x] Implement `project_sync()` (one-way: local → cloud) - [x] Implement `project_bisync()` (two-way: local ↔ cloud) - [x] Implement `project_check()` (integrity verification) - [x] Implement `project_ls()` (list remote files) - [x] Add helper: `get_project_bisync_state(project_name)` - [x] Add helper: `bisync_initialized(project_name)` - [x] Add helper: `get_bmignore_filter_path()` - [x] Add `SyncProject` dataclass for project representation - [x] Write unit tests for rclone commands (22 tests, 99% coverage) - [x] Temporarily disable mount commands in core_commands.py ### Phase 4: CLI Integration ✅ - [x] Update `project.py`: Add `--local-path` flag to `project add` command - [x] Update `project.py`: Create `project sync-setup` command - [x] Create `project.py`: Add `project sync` command - [x] Create `project.py`: Add `project bisync` command - [x] Create `project.py`: Add `project check` command - [x] Create `project.py`: Add `project ls` command - [x] Create `project.py`: Add `project bisync-reset` command - [x] Import rclone_commands module and get_mount_info helper - [x] Update `project list` to show local sync paths in cloud mode - [x] Update `project list` to conditionally show columns based on config - [x] Update `project remove` to clean up local directories and bisync state - [x] Add automatic database sync trigger after file sync operations - [x] Add path normalization to prevent S3 mount point leakage - [x] Update `cloud/core_commands.py`: Simplified `cloud setup` command - [x] Write unit tests for `project add --local-path` (4 tests passing) ### Phase 5: Cleanup ✅ - [x] Remove `mount_commands.py` (entire file) - [x] Remove mount-related functions from `rclone_config.py`: - [x] `MOUNT_PROFILES` - [x] `get_default_mount_path()` - [x] `build_mount_command()` - [x] `is_path_mounted()` - [x] `get_rclone_processes()` - [x] `kill_rclone_process()` - [x] `unmount_path()` - [x] `cleanup_orphaned_rclone_processes()` - [x] Remove from `bisync_commands.py`: - [x] `BISYNC_PROFILES` (use single default) - [x] `setup_cloud_bisync()` - [x] `run_bisync_watch()` - [x] `show_bisync_status()` - [x] `run_bisync()` - [x] `run_check()` - [x] Remove `bisync_config` from config schema - [x] Remove deprecated cloud commands: - [x] `cloud mount` - [x] `cloud unmount` - [x] Simplified `cloud setup` to just install rclone and configure credentials - [x] Remove convenience commands: - [x] Root-level `bm sync` (removed - confusing in cloud mode, automatic in local mode) - [x] Update tests to remove references to deprecated functionality - [x] All typecheck errors resolved ### Phase 6: Documentation ✅ - [x] Update `docs/cloud-cli.md` with new workflow - [x] Add troubleshooting section for empty directory issues - [x] Add troubleshooting section for bisync state corruption - [x] Document `bisync-reset` command usage - [x] Update command reference with all new commands - [x] Add examples for common workflows - [ ] Add migration guide for existing users (deferred - no users on old system yet) - [ ] Update SPEC-8 with "Superseded by SPEC-20" note (deferred) ### Testing & Validation ✅ - [x] Test Scenario 1: New user setup (manual testing complete) - [x] Test Scenario 2: Multiple projects (manual testing complete) - [x] Test Scenario 3: Project without sync (manual testing complete) - [x] Test Scenario 4: Integrity check (manual testing complete) - [x] Test Scenario 5: bisync-reset command (manual testing complete) - [x] Test cleanup on remove (manual testing complete) - [x] Verify all commands work end-to-end - [x] Document known issues (empty directory bisync limitation) - [ ] Automated integration tests (deferred) - [ ] Test migration from SPEC-8 implementation (N/A - no users yet) ## Implementation Notes ### Key Improvements Added During Implementation **1. Path Normalization (Critical Bug Fix)** **Problem:** Files were syncing to `/app/data/app/data/project/` instead of `/app/data/project/` **Root cause:** - S3 bucket contains projects directly (e.g., `basic-memory-llc/`) - Fly machine mounts bucket at `/app/data/` - API returns paths like `/app/data/basic-memory-llc` (mount point + project) - Rclone was using this full path, causing path doubling **Solution (three layers):** - API side: Added `normalize_project_path()` in `project_router.py` to strip `/app/data/` prefix - CLI side: Added defensive normalization in `project.py` commands - Rclone side: Updated `get_project_remote()` to strip prefix before building remote path **Files modified:** - `src/basic_memory/api/routers/project_router.py` - API normalization - `src/basic_memory/cli/commands/project.py` - CLI normalization - `src/basic_memory/cli/commands/cloud/rclone_commands.py` - Rclone remote path construction **2. Automatic Database Sync After File Operations** **Enhancement:** After successful file sync or bisync, automatically trigger database sync via API **Implementation:** - After `project sync`: POST to `/{project}/project/sync` - After `project bisync`: POST to `/{project}/project/sync` + update config timestamps - Skip trigger on `--dry-run` - Graceful error handling with warnings **Benefit:** Files and database stay in sync automatically without manual intervention **3. Enhanced Project Removal with Cleanup** **Enhancement:** `bm project remove` now properly cleans up local artifacts **Behavior with `--delete-notes`:** - ✓ Removes project from cloud API - ✓ Deletes cloud files - ✓ Removes local sync directory - ✓ Removes bisync state directory - ✓ Removes `cloud_projects` config entry **Behavior without `--delete-notes`:** - ✓ Removes project from cloud API - ✗ Keeps local files (shows path in message) - ✓ Removes bisync state directory (cleanup) - ✓ Removes `cloud_projects` config entry **Files modified:** - `src/basic_memory/cli/commands/project.py` - Enhanced `remove_project()` function **4. Bisync State Reset Command** **New command:** `bm project bisync-reset <project>` **Purpose:** Clear bisync state when it becomes corrupted (e.g., after mixing dry-run and actual runs) **What it does:** - Removes all bisync metadata from `~/.basic-memory/bisync-state/{project}/` - Forces fresh baseline on next `--resync` - Safe operation (doesn't touch files) - Also runs automatically on project removal **Files created:** - Added `bisync-reset` command to `src/basic_memory/cli/commands/project.py` **5. Improved UI for Project List** **Enhancements:** - Shows "Local Path" column in cloud mode for projects with sync configured - Conditionally shows/hides columns based on config: - Local Path: only in cloud mode - Default: only when `default_project_mode` is True - Uses `no_wrap=True, overflow="fold"` to prevent path truncation - Applies path normalization to prevent showing mount point details **Files modified:** - `src/basic_memory/cli/commands/project.py` - Enhanced `list_projects()` function **6. Documentation of Known Issues** **Issue documented:** Rclone bisync limitation with empty directories **Problem:** "Empty prior Path1 listing. Cannot sync to an empty directory" **Explanation:** Bisync creates listing files that track state. When both directories are completely empty, these listing files are considered invalid. **Solution documented:** Add at least one file (like README.md) before running `--resync` **Files updated:** - `docs/cloud-cli.md` - Added troubleshooting sections for: - Empty directory issues - Bisync state corruption - Usage of `bisync-reset` command ### Rclone Flag Fix **Bug fix:** Incorrect rclone flag causing sync failures **Error:** `unknown flag: --filters-file` **Fix:** Changed `--filters-file` to correct flag `--filter-from` in both `project_sync()` and `project_bisync()` functions **Files modified:** - `src/basic_memory/cli/commands/cloud/rclone_commands.py` ### Test Coverage **Unit tests added:** - `tests/cli/test_project_add_with_local_path.py` - 4 tests for `--local-path` functionality - Test with local path saves to config - Test without local path doesn't save to config - Test tilde expansion in paths - Test nested directory creation **Manual testing completed:** - All 10 project commands tested end-to-end - Path normalization verified - Database sync trigger verified - Cleanup on remove verified - Bisync state reset verified ## Future Enhancements (Out of Scope) - **Per-project rclone profiles**: Allow advanced users to override defaults - **Conflict resolution UI**: Interactive conflict resolution for bisync - **Sync scheduling**: Automatic periodic sync without watch mode - **Sync analytics**: Track sync frequency, data transferred, etc. - **Multi-machine coordination**: Detect and warn about concurrent edits from different machines

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/basicmachines-co/basic-memory'

If you have feedback or need assistance with the MCP directory API, please join our Discord server