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
13 changes: 6 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,16 +438,15 @@ See `.claude/commands/release/release.md` (and `beta.md`, `release-check.md`, `c
- Setup cloud sync: `basic-memory cloud setup`
- Save API key: `basic-memory cloud api-key save bmc_...`
- Create API key: `basic-memory cloud api-key create "name"`
- Integrity check (local vs cloud): `basic-memory cloud check --name "name"`
- Integrity check (local vs cloud, legacy Personal-only): `basic-memory cloud check --name "name"`
- Manage snapshots: `basic-memory cloud snapshot [create|list|delete|show|browse]`
- Restore from snapshot: `basic-memory cloud restore <path> --snapshot <id>`

**Cloud Sync Commands (Personal and Team workspaces):**
- Fetch cloud changes (cloud -> local): `basic-memory cloud pull --name "name"` (Team-safe; additive, never deletes local)
- Upload local changes (local -> cloud): `basic-memory cloud push --name "name"` (Team-safe; additive, never deletes cloud)
- Resolve conflicts on push/pull: `--on-conflict [fail|keep-local|keep-cloud|keep-both]` (default `fail` lists conflicts and aborts, git-style)
- One-way mirror (local -> cloud): `basic-memory cloud sync --name "name"` (Personal workspaces only; deletes cloud files missing locally)
- Two-way mirror (local <-> cloud): `basic-memory cloud bisync --name "name"` (Personal workspaces only)
- Deprecated (#1596), Personal workspaces only, to be removed: `basic-memory cloud sync` (one-way mirror; deletes cloud files missing locally), `basic-memory cloud bisync` and `bisync-reset` (two-way mirror). They warn on every run; use pull/push.

### MCP Capabilities

Expand All @@ -465,10 +464,10 @@ Basic Memory now supports cloud synchronization and storage (requires active sub
- Secure session management with token refresh
- Support for multiple cloud projects

**Bidirectional Sync:**
- rclone bisync integration for two-way synchronization
- Conflict resolution and integrity verification
- Real-time sync with change detection
**File Sync:**
- `bm cloud pull` / `bm cloud push`: additive, git-style transfers on Personal and Team workspaces
- Conflict resolution with `--on-conflict`
- rclone bisync is deprecated (#1596)

**Cloud Project Management:**
- Create and manage projects in the cloud
Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@
only `workspace` and `shared` visibility, so `private` had created a team-visible
project while reporting success.

- **#1596**: `bm cloud sync`, `bm cloud bisync` and `bm cloud bisync-reset` are
deprecated and will be removed in a future release. `bm cloud pull` / `bm cloud push`
are the supported sync workflow on Personal and Team workspaces. The mirror commands
are marked deprecated in `--help`, print a notice with the pull/push command on every
run, still work on Personal workspaces, and on Team workspaces exit with that notice
instead of a "Personal only" error. `bm project list` no longer shows Team projects
with a local sync path as `cloud-only`: `sync_supported` is now true for every
workspace, and `list_memory_projects` reports the same. Help examples no longer pass
`--workspace Personal`.

### Features

- **#1642**: `write_note` can overwrite only the revision you read. Pass
Expand Down Expand Up @@ -332,6 +342,18 @@

### Bug Fixes

- **#1595**: `basic_memory_diagnostics` lists the `BASIC_MEMORY_*` environment
variables that override `config.json`, redacted the same way as the file dump. It
used to print only the file, which can disagree with what the server is using. Other
`BASIC_MEMORY_*` variables are listed by name only. Env names now match config fields
in any letter case, as pydantic-settings reads them: a lowercase
`basic_memory_log_level` used to lose to the file value, or, when the file lacked the
key, take effect and then get written into `config.json`.

- **#1593**: The CLI's list of commands that skip startup initialization no longer
names `sync` and `watch`, which are not commands. A test now keeps the list to
registered commands. Thanks to @FBISiri.

- `bm tool edit-note --help` lists all six edit operations and documents `--section` for
`replace_section`, `insert_before_section` and `insert_after_section`. Thanks to
@xhkzdepartedream (#1708).
Expand Down
459 changes: 169 additions & 290 deletions docs/cloud-cli.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions integrations/hermes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ In local mode the plugin auto-creates the BM project on first init via `bm proje

### Cloud mode

When `mode: cloud`, tool calls route directly through the BM cloud API — no local file mirror, no bisync. You set this up once with the BM CLI:
When `mode: cloud`, tool calls route directly through the BM cloud API — no local file mirror, no file sync. You set this up once with the BM CLI:

```bash
# Authenticate (OAuth) or save an API key
Expand Down Expand Up @@ -192,7 +192,7 @@ hermes gateway restart

Tool calls now route from `bm mcp` → `<cloud_host>/proxy` over HTTPS using your OAuth token (or API key). Notes never touch local disk.

**Don't confuse cloud mode with `bm cloud bisync`.** Bisync is rclone-style two-way file sync between a *local* project and cloud storage, intended for keeping local working copies. For agent-driven capture you want true cloud routing (`set-cloud`), not bisync.
**Don't confuse cloud mode with file sync.** `bm cloud pull` / `bm cloud push` copy files between a *local* project and cloud storage, for keeping local working copies (`bm cloud bisync` is deprecated). For agent-driven capture you want true cloud routing (`set-cloud`), not file sync.

## Updating / removing

Expand Down
8 changes: 4 additions & 4 deletions integrations/openclaw/BASIC_MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ bm project info
# Set the default project
bm project default "name"

# One-way sync (local -> cloud)
bm project sync
# Fetch cloud changes into a local project (cloud -> local)
bm cloud pull --name "name"

# Bidirectional sync
bm project bisync
# Upload local changes to cloud (local -> cloud)
bm cloud push --name "name"
```

## Cross-Project Operations
Expand Down
73 changes: 38 additions & 35 deletions src/basic_memory/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,43 @@ def version_callback(value: bool) -> None:
raise typer.Exit()


# Top-level commands that skip ensure_initialization() in app_callback.
# Skip for 'mcp' command - it has its own lifespan that handles initialization
# Skip for API-using commands (status, tool, etc.) - they handle initialization via deps.py
# Skip for 'reset' command - it manages its own database lifecycle
# Skip for 'man' - it only copies packaged files; a broken local database
# must not block installing the offline docs
# ('hook' returns before this check.)
# Every entry must be a registered top-level command (tests/cli/test_cli_app.py).
SKIP_INIT_COMMANDS = frozenset(
{
"doctor",
"inspect",
"man",
"mcp",
"status",
"project",
"config",
"tool",
"reset",
"reindex",
"prune",
"update",
"wiki",
"workspace",
# POSIX read verbs (#1404): API-hitting commands that initialize via
# deps.py, same as status/tool.
"cat",
"grep",
"ls",
"find",
"tail",
"head",
"tree",
}
)


app = typer.Typer(name="basic-memory")


Expand Down Expand Up @@ -127,44 +164,10 @@ def _post_command_messages() -> None:

ctx.call_on_close(_post_command_messages)

# Run initialization for commands that don't use the API
# Skip for 'mcp' command - it has its own lifespan that handles initialization
# Skip for API-using commands (status, sync, etc.) - they handle initialization via deps.py
# Skip for 'reset' command - it manages its own database lifecycle
# Skip for 'man' - it only copies packaged files; a broken local database
# must not block installing the offline docs
# ('hook' returns above, before this point.)
skip_init_commands = {
"doctor",
"inspect",
"man",
"mcp",
"status",
"sync",
"project",
"config",
"tool",
"reset",
"reindex",
"prune",
"update",
"watch",
"wiki",
"workspace",
# POSIX read verbs (#1404): API-hitting commands that initialize via
# deps.py, same as status/tool.
"cat",
"grep",
"ls",
"find",
"tail",
"head",
"tree",
}
if (
not version
and ctx.invoked_subcommand is not None
and ctx.invoked_subcommand not in skip_init_commands
and ctx.invoked_subcommand not in SKIP_INIT_COMMANDS
):
from basic_memory.services.initialization import ensure_initialization

Expand Down
68 changes: 43 additions & 25 deletions src/basic_memory/cli/commands/cloud/project_sync.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""Cloud sync commands for Basic Memory projects.

Commands for syncing, bisyncing, and checking integrity between local and cloud
project instances. These were previously in project.py but belong here since
they are cloud-specific operations.
`bm cloud pull` / `bm cloud push` are the supported way to move files between a
local project and Basic Memory Cloud, on Personal and Team workspaces alike. The
rclone mirror commands (`sync`, `bisync`, `bisync-reset`) are deprecated (#1596):
they still run on Personal workspaces, warn on every run, and refuse Team
workspaces with the push/pull command to use instead.
"""

import os
Expand Down Expand Up @@ -59,14 +61,20 @@

console = Console()

MIRROR_DEPRECATION_NOTICE = (
"`bm cloud {command}` is deprecated and will be removed in a future release.\n"
"Use `bm cloud pull --name {name}` (fetch) / `bm cloud push --name {name}` "
"(additive upload) instead. They work on Personal and Team workspaces."
)

TEAM_WORKSPACE_BISYNC_UNSUPPORTED = (
"The bisync operation is only supported on Personal workspaces.\n"
"`bm cloud bisync` is deprecated and does not run on Team workspaces.\n"
"Use `bm cloud pull --name {name}` / `bm cloud push --name {name}` instead."
)

TEAM_WORKSPACE_SYNC_UNSUPPORTED = (
"The sync operation mirrors local onto the shared bucket and can delete a "
"teammate's files, so it is only supported on Personal workspaces.\n"
"`bm cloud sync` is deprecated and does not run on Team workspaces: it mirrors "
"local onto the shared bucket and can delete a teammate's files.\n"
"Use `bm cloud pull --name {name}` (fetch) / `bm cloud push --name {name}` "
"(additive upload) instead."
)
Expand Down Expand Up @@ -101,6 +109,14 @@ class ConflictStrategy(str, Enum):
# --- Shared helpers ---


def _warn_mirror_deprecated(command: str, name: str) -> None:
"""Print the deprecation notice every deprecated mirror command shows on each run."""
notice = MIRROR_DEPRECATION_NOTICE.format(command=command, name=shlex.quote(name))
# markup=False: the project name is user text, and a name like `[bold]x[/bold]`
# would otherwise render as `x`, so the copied pull/push command would be wrong.
console.print(notice, style="yellow", markup=False)


def _has_cloud_credentials(config: BasicMemoryConfig) -> bool:
"""Return whether cloud credentials are available (API key or OAuth token)."""
from basic_memory.config import has_cloud_credentials
Expand Down Expand Up @@ -201,7 +217,7 @@ def _require_personal_workspace(
if workspace.workspace_type != "personal":
# The templates below embed `--name {name}`; quote it before rendering so a
# name with a space stays one argument in the command they print.
console.print(f"[red]{unsupported_message.format(name=shlex.quote(name))}[/red]")
console.print(unsupported_message.format(name=shlex.quote(name)), style="red", markup=False)
raise typer.Exit(1)

return workspace
Expand Down Expand Up @@ -272,15 +288,15 @@ def _get_sync_project(
# --- Commands ---


@cloud_app.command("sync")
@cloud_app.command("sync", deprecated=True)
def sync_project_command(
name: str = typer.Option(..., "--name", "--project", help="Project name to sync"),
dry_run: bool = typer.Option(False, "--dry-run", help="Preview changes without syncing"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
) -> None:
"""One-way mirror: local -> cloud (make cloud identical to local).

Personal workspaces only. This deletes cloud files not present locally —
Use `bm cloud push` / `bm cloud pull` instead. Personal workspaces only. This deletes cloud files not present locally —
including files matching .bmignore, even if they synced before the pattern
was added — so on Team workspaces use `bm cloud push` (additive upload) /
`bm cloud pull` (fetch) instead. Preview deletions with --dry-run.
Expand All @@ -290,6 +306,8 @@ def sync_project_command(
bm cloud sync --name research --dry-run
"""
config = ConfigManager().config
# The migration notice comes first so a missing login still shows pull/push.
_warn_mirror_deprecated("sync", name)
Comment thread
phernandez marked this conversation as resolved.
_require_cloud_credentials(config)
target_workspace = _require_personal_workspace(
name,
Expand Down Expand Up @@ -782,7 +800,7 @@ def push_project_command(
)


@cloud_app.command("bisync")
@cloud_app.command("bisync", deprecated=True)
def bisync_project_command(
name: str = typer.Option(..., "--name", "--project", help="Project name to bisync"),
dry_run: bool = typer.Option(False, "--dry-run", help="Preview changes without syncing"),
Expand All @@ -791,16 +809,18 @@ def bisync_project_command(
) -> None:
"""Two-way mirror: local <-> cloud (bidirectional sync).

Personal workspaces only. This mirror can delete and overwrite files on both
sides, so on Team workspaces use `bm cloud pull` (fetch) / `bm cloud push`
(additive upload) instead.
Use `bm cloud pull` (fetch) / `bm cloud push` (additive upload) instead.
Personal workspaces only: this mirror can delete and overwrite files on
both sides.

Examples:
bm cloud bisync --name research --resync # First time
bm cloud bisync --name research # Subsequent syncs
bm cloud bisync --name research --dry-run # Preview changes
"""
config = ConfigManager().config
# The migration notice comes first so a missing login still shows pull/push.
_warn_mirror_deprecated("bisync", name)
_require_cloud_credentials(config)
_require_personal_workspace(name, config)

Expand Down Expand Up @@ -859,9 +879,10 @@ def check_project_command(
) -> None:
"""Verify file integrity between local and cloud (no changes made).

Personal workspaces only: check compares against the Personal workspace
mirror remote. On Team workspaces use `bm cloud pull --dry-run` /
`bm cloud push --dry-run` to preview differences instead.
Legacy, Personal workspaces only: check compares against the Personal
workspace mirror remote used by the deprecated `sync` / `bisync` commands.
Use `bm cloud pull --dry-run` / `bm cloud push --dry-run` to preview
differences instead.

Example:
bm cloud check --name research
Expand Down Expand Up @@ -903,21 +924,22 @@ def check_project_command(
raise typer.Exit(1)


@cloud_app.command("bisync-reset")
@cloud_app.command("bisync-reset", deprecated=True)
def bisync_reset(
name: str = typer.Argument(..., help="Project name to reset bisync state for"),
) -> None:
"""Clear bisync state for a project.

Personal workspaces only (bisync is a Personal-workspace mirror; on Team
workspaces use `bm cloud pull` / `bm cloud push` instead).
`bm cloud bisync` is deprecated; use `bm cloud pull` / `bm cloud push`
instead. Personal workspaces only.

This removes the bisync metadata files, forcing a fresh --resync on next bisync.
Useful when bisync gets into an inconsistent state or when remote path changes.
"""
import shutil

config = ConfigManager().config
_warn_mirror_deprecated("bisync-reset", name)
if _has_cloud_credentials(config):
_require_personal_workspace(name, config)

Expand Down Expand Up @@ -1008,8 +1030,8 @@ async def _create_local_project():

console.print(f"[green]Sync configured for project '{name}'[/green]")
console.print(f"\nLocal sync path: {resolved_path}")
# Lead with the Team-safe additive commands (work on any workspace); the
# `sync`/`bisync` mirrors are Personal-workspace-only.
# Push/pull is the supported sync workflow on every workspace; the
# `sync`/`bisync` mirrors are deprecated (#1596), so they are not suggested.
console.print("\nNext steps:")
console.print(
f" 1. Preview a pull: {shell_command('bm', 'cloud', 'pull', '--name', name, '--dry-run')}"
Expand All @@ -1020,10 +1042,6 @@ async def _create_local_project():
console.print(
f" 3. Upload local changes: {shell_command('bm', 'cloud', 'push', '--name', name)}"
)
console.print(
f" Personal workspaces can also mirror with: "
f"{shell_command('bm', 'cloud', 'bisync', '--name', name, '--resync')}"
)
except Exception as e:
console.print(f"[red]Error configuring sync: {str(e)}[/red]")
raise typer.Exit(1)
2 changes: 1 addition & 1 deletion src/basic_memory/cli/commands/cloud/rclone_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
- Routes through the project's tenant-scoped remote (SyncProject.remote_name);
the default tenant keeps "basic-memory-cloud", others use their own (see #919)
- Balanced defaults from SPEC-8 Phase 4 testing
- Per-project bisync state tracking
- Per-project bisync state tracking (for the deprecated `bm cloud bisync`, #1596)

Replaces tenant-wide sync with project-scoped workflows.
"""
Expand Down
Loading
Loading