Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions docs/cloud-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The Basic Memory Cloud CLI provides seamless integration between local and cloud
The cloud CLI enables you to:
- **Toggle cloud mode** with `bm cloud login` / `bm cloud logout`
- **Use regular commands in cloud mode**: `bm project`, `bm sync`, `bm tool` all work with cloud
- **Upload local files** directly to cloud projects via `bm cloud upload`
- **Bidirectional sync** with rclone bisync (recommended for most users)
- **Direct file access** via rclone mount (alternative workflow)
- **Integrity verification** with `bm cloud check`
Expand Down Expand Up @@ -160,6 +161,69 @@ bm project list

This Dropbox-like workflow means you don't need to manually coordinate projects between local and cloud.

### Uploading Local Files

You can directly upload local files or directories to cloud projects using `bm cloud upload`. This is useful for:
- Migrating existing local projects to the cloud
- Quickly uploading specific files or directories
- One-time bulk uploads without setting up sync

**Basic Usage:**

```bash
# Upload a directory to existing project
bm cloud upload ~/my-notes --project research

# Upload a single file
bm cloud upload important-doc.md --project research
```

**Create Project On-the-Fly:**

If the target project doesn't exist yet, use `--create-project`:

```bash
# Upload and create project in one step
bm cloud upload ~/local-project --project new-research --create-project
```

**Skip Automatic Sync:**

By default, the command syncs the project after upload to index the files. To skip this:

```bash
# Upload without triggering sync
bm cloud upload ~/bulk-data --project archives --no-sync
```

**File Filtering:**

The upload command respects `.bmignore` and `.gitignore` patterns, automatically excluding:
- Hidden files (`.git`, `.DS_Store`)
- Build artifacts (`node_modules`, `__pycache__`)
- Database files (`*.db`, `*.db-wal`)
- Environment files (`.env`)

To customize what gets uploaded, edit `~/.basic-memory/.bmignore`.

**Complete Example:**

```bash
# 1. Login to cloud
bm cloud login

# 2. Upload local project (creates project if needed)
bm cloud upload ~/Documents/research-notes --project research --create-project

# 3. Verify upload
bm project list
```

**Notes:**
- Files are uploaded directly via WebDAV (no sync setup required)
- Uploads are immediate and don't require bisync or mount
- Use this for migration or one-time uploads; use `bm sync` for ongoing synchronization

## File Synchronization

### The `bm sync` Command (Cloud Mode Aware)
Expand Down Expand Up @@ -628,6 +692,15 @@ bm cloud check # Full integrity check
bm cloud check --one-way # Faster one-way check
```

### File Upload

```bash
# Upload files/directories to cloud projects
bm cloud upload <path> --project <name> # Upload to existing project
bm cloud upload <path> -p <name> --create-project # Upload and create project
bm cloud upload <path> -p <name> --no-sync # Upload without syncing
```

### Direct File Access (Mount)

```bash
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ dependencies = [
"pyjwt>=2.10.1",
"python-dotenv>=1.1.0",
"pytest-aio>=1.9.0",
"aiofiles>=24.1.0", # Async file I/O
"aiofiles>=24.1.0", # Async file I/O
]


Expand Down
3 changes: 2 additions & 1 deletion src/basic_memory/cli/commands/cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@

# Import all commands to register them with typer
from basic_memory.cli.commands.cloud.core_commands import * # noqa: F401,F403
from basic_memory.cli.commands.cloud.api_client import get_authenticated_headers # noqa: F401
from basic_memory.cli.commands.cloud.api_client import get_authenticated_headers, get_cloud_config # noqa: F401
from basic_memory.cli.commands.cloud.upload_command import * # noqa: F401,F403
61 changes: 4 additions & 57 deletions src/basic_memory/cli/commands/cloud/bisync_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
from rich.table import Table

from basic_memory.cli.commands.cloud.api_client import CloudAPIError, make_api_request
from basic_memory.cli.commands.cloud.cloud_utils import (
create_cloud_project,
fetch_cloud_projects,
)
from basic_memory.cli.commands.cloud.rclone_config import (
add_tenant_to_rclone_config,
)
Expand All @@ -21,11 +25,7 @@
from basic_memory.schemas.cloud import (
TenantMountInfo,
MountCredentials,
CloudProjectList,
CloudProjectCreateRequest,
CloudProjectCreateResponse,
)
from basic_memory.utils import generate_permalink

console = Console()

Expand Down Expand Up @@ -110,24 +110,6 @@ async def generate_mount_credentials(tenant_id: str) -> MountCredentials:
raise BisyncError(f"Failed to generate credentials: {e}") from e


async def fetch_cloud_projects() -> CloudProjectList:
"""Fetch list of projects from cloud API.

Returns:
CloudProjectList with projects from cloud
"""
try:
config_manager = ConfigManager()
config = config_manager.config
host_url = config.cloud_host.rstrip("/")

response = await make_api_request(method="GET", url=f"{host_url}/proxy/projects/projects")

return CloudProjectList.model_validate(response.json())
except Exception as e:
raise BisyncError(f"Failed to fetch cloud projects: {e}") from e


def scan_local_directories(sync_dir: Path) -> list[str]:
"""Scan local sync directory for project folders.

Expand All @@ -148,41 +130,6 @@ def scan_local_directories(sync_dir: Path) -> list[str]:
return directories


async def create_cloud_project(project_name: str) -> CloudProjectCreateResponse:
"""Create a new project on cloud.

Args:
project_name: Name of project to create

Returns:
CloudProjectCreateResponse with project details from API
"""
try:
config_manager = ConfigManager()
config = config_manager.config
host_url = config.cloud_host.rstrip("/")

# Use generate_permalink to ensure consistent naming
project_path = generate_permalink(project_name)

project_data = CloudProjectCreateRequest(
name=project_name,
path=project_path,
set_default=False,
)

response = await make_api_request(
method="POST",
url=f"{host_url}/proxy/projects/projects",
headers={"Content-Type": "application/json"},
json_data=project_data.model_dump(),
)

return CloudProjectCreateResponse.model_validate(response.json())
except Exception as e:
raise BisyncError(f"Failed to create cloud project '{project_name}': {e}") from e


def get_bisync_state_path(tenant_id: str) -> Path:
"""Get path to bisync state directory."""
return Path.home() / ".basic-memory" / "bisync-state" / tenant_id
Expand Down
100 changes: 100 additions & 0 deletions src/basic_memory/cli/commands/cloud/cloud_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Shared utilities for cloud operations."""

from basic_memory.cli.commands.cloud.api_client import make_api_request
from basic_memory.config import ConfigManager
from basic_memory.schemas.cloud import (
CloudProjectList,
CloudProjectCreateRequest,
CloudProjectCreateResponse,
)
from basic_memory.utils import generate_permalink


class CloudUtilsError(Exception):
"""Exception raised for cloud utility errors."""

pass


async def fetch_cloud_projects() -> CloudProjectList:
"""Fetch list of projects from cloud API.

Returns:
CloudProjectList with projects from cloud
"""
try:
config_manager = ConfigManager()
config = config_manager.config
host_url = config.cloud_host.rstrip("/")

response = await make_api_request(method="GET", url=f"{host_url}/proxy/projects/projects")

return CloudProjectList.model_validate(response.json())
except Exception as e:
raise CloudUtilsError(f"Failed to fetch cloud projects: {e}") from e


async def create_cloud_project(project_name: str) -> CloudProjectCreateResponse:
"""Create a new project on cloud.

Args:
project_name: Name of project to create

Returns:
CloudProjectCreateResponse with project details from API
"""
try:
config_manager = ConfigManager()
config = config_manager.config
host_url = config.cloud_host.rstrip("/")

# Use generate_permalink to ensure consistent naming
project_path = generate_permalink(project_name)

project_data = CloudProjectCreateRequest(
name=project_name,
path=project_path,
set_default=False,
)

response = await make_api_request(
method="POST",
url=f"{host_url}/proxy/projects/projects",
headers={"Content-Type": "application/json"},
json_data=project_data.model_dump(),
)

return CloudProjectCreateResponse.model_validate(response.json())
except Exception as e:
raise CloudUtilsError(f"Failed to create cloud project '{project_name}': {e}") from e


async def sync_project(project_name: str) -> None:
"""Trigger sync for a specific project on cloud.

Args:
project_name: Name of project to sync
"""
try:
from basic_memory.cli.commands.command_utils import run_sync

await run_sync(project=project_name)
except Exception as e:
raise CloudUtilsError(f"Failed to sync project '{project_name}': {e}") from e


async def project_exists(project_name: str) -> bool:
"""Check if a project exists on cloud.

Args:
project_name: Name of project to check

Returns:
True if project exists, False otherwise
"""
try:
projects = await fetch_cloud_projects()
project_names = {p.name for p in projects.projects}
return project_name in project_names
except Exception:
return False
Loading
Loading