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
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@

## 🚀 Basic Memory Cloud is Live!

- **Cross-device and multi-platform support is here.** Your knowledge graph now works on desktop, web, and mobile - seamlessly synced across all your AI tools (Claude, ChatGPT, Gemini, Claude Code, and Codex)
- **Early Supporter Pricing:** Early users get 25% off forever.
The open source project continues as always. Cloud just makes it work everywhere.
- **Cross-device and multi-platform support is here.** Your knowledge graph now works on desktop, web, and mobile.
- **Cloud is optional.** The local-first open-source workflow continues as always.
- **OSS discount:** use code `{{OSS_DISCOUNT_CODE}}` for 20% off for 3 months.

[Sign up now →](https://basicmemory.com)

Expand Down Expand Up @@ -408,6 +408,12 @@ get_current_project() - Show current project stats
sync_status() - Check synchronization status
```

**Cloud Discovery (opt-in):**
```
cloud_info() - Show optional Cloud overview and setup guidance
release_notes() - Show latest release notes
```

**Visualization:**
```
canvas(nodes, edges, title, folder) - Generate knowledge visualizations
Expand Down
4 changes: 4 additions & 0 deletions docs/cloud-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Before using Basic Memory Cloud, you need:

- **Active Subscription**: An active Basic Memory Cloud subscription is required to access cloud features
- **Subscribe**: Visit [https://basicmemory.com/subscribe](https://basicmemory.com/subscribe) to sign up
- **Optional**: Cloud is optional. Local-first open-source usage continues without cloud.
- **OSS Discount**: Use code `{{OSS_DISCOUNT_CODE}}` for 20% off for 3 months.

If you attempt to log in without an active subscription, you'll receive a "Subscription Required" error with a link to subscribe.

Expand Down Expand Up @@ -81,6 +83,7 @@ bm cloud login
4. Validates your subscription status

**Result:** All `bm project`, `bm tools` commands now work with cloud.
Apply OSS discount code `{{OSS_DISCOUNT_CODE}}` during checkout to receive 20% off for 3 months.

### 2. Set Up Sync

Expand Down Expand Up @@ -659,6 +662,7 @@ If instance is down, wait a few minutes and retry.
bm cloud login # Authenticate and enable cloud mode
bm cloud logout # Disable cloud mode
bm cloud status # Check cloud mode and instance health
bm cloud promo --off # Disable CLI cloud promo notices
```

### Setup
Expand Down
3 changes: 3 additions & 0 deletions src/basic_memory/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import typer # noqa: E402

from basic_memory.cli.container import CliContainer, set_container # noqa: E402
from basic_memory.cli.promo import maybe_show_cloud_promo # noqa: E402
from basic_memory.config import init_cli_logging # noqa: E402


Expand Down Expand Up @@ -46,6 +47,8 @@ def app_callback(
container = CliContainer.create()
set_container(container)

maybe_show_cloud_promo(ctx.invoked_subcommand)

# 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
Expand Down
19 changes: 19 additions & 0 deletions src/basic_memory/cli/commands/cloud/core_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from basic_memory.cli.app import cloud_app
from basic_memory.cli.commands.command_utils import run_with_cleanup
from basic_memory.cli.auth import CLIAuth
from basic_memory.cli.promo import OSS_DISCOUNT_CODE
from basic_memory.config import ConfigManager
from basic_memory.cli.commands.cloud.api_client import (
CloudAPIError,
Expand Down Expand Up @@ -57,6 +58,10 @@ async def _login():
except SubscriptionRequiredError as e:
console.print("\n[red]Subscription Required[/red]\n")
console.print(f"[yellow]{e.args[0]}[/yellow]\n")
console.print(
f"OSS discount code: [bold]{OSS_DISCOUNT_CODE}[/bold] "
"(20% off for 3 months)\n"
)
console.print(f"Subscribe at: [blue underline]{e.subscribe_url}[/blue underline]\n")
console.print(
"[dim]Once you have an active subscription, run [bold]bm cloud login[/bold] again.[/dim]"
Expand Down Expand Up @@ -191,3 +196,17 @@ def setup() -> None:
except Exception as e:
console.print(f"\n[red]Unexpected error during setup: {e}[/red]")
raise typer.Exit(1)


@cloud_app.command("promo")
def promo(enabled: bool = typer.Option(True, "--on/--off", help="Enable or disable CLI promos.")):
"""Enable or disable CLI cloud promo messages."""
config_manager = ConfigManager()
config = config_manager.load_config()
config.cloud_promo_opt_out = not enabled
config_manager.save_config(config)

if enabled:
console.print("[green]Cloud promo messages enabled[/green]")
else:
console.print("[yellow]Cloud promo messages disabled[/yellow]")
84 changes: 84 additions & 0 deletions src/basic_memory/cli/promo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Cloud promo messaging for CLI entrypoint."""

import os
import sys
from collections.abc import Callable

import typer

from basic_memory.config import ConfigManager

CLOUD_PROMO_VERSION = "2026-02-06"
OSS_DISCOUNT_CODE = "{{OSS_DISCOUNT_CODE}}"


def _promos_disabled_by_env() -> bool:
"""Check environment-level kill switch for promo output."""
value = os.getenv("BASIC_MEMORY_NO_PROMOS", "").strip().lower()
return value in {"1", "true", "yes"}


def _is_interactive_session() -> bool:
"""Return whether stdin/stdout are interactive terminals."""
return sys.stdin.isatty() and sys.stdout.isatty()


def _build_first_run_message() -> str:
"""Build first-run cloud promo copy."""
return (
"Basic Memory initialized (local mode).\n"
"Cloud is optional and keeps your workflow local-first.\n"
"Cloud adds cross-device sync + mobile/web access.\n"
f"OSS discount: {OSS_DISCOUNT_CODE} (20% off for 3 months).\n"
"Run `bm cloud login` to enable."
)


def _build_version_message() -> str:
"""Build cloud promo copy shown after promo-version bumps."""
return (
"New in Basic Memory Cloud: cross-device sync + mobile/web access.\n"
f"OSS discount: {OSS_DISCOUNT_CODE} (20% off for 3 months).\n"
"Run `bm cloud login` to enable."
)


def maybe_show_cloud_promo(
invoked_subcommand: str | None,
*,
config_manager: ConfigManager | None = None,
is_interactive: bool | None = None,
echo: Callable[[str], None] = typer.echo,
) -> None:
"""Show cloud promo copy when discovery gates are satisfied."""
manager = config_manager or ConfigManager()
config = manager.load_config()

interactive = _is_interactive_session() if is_interactive is None else is_interactive

# Trigger: environment-level promo suppression or non-interactive execution.
# Why: avoid polluting scripts/CI output and support a hard opt-out.
# Outcome: skip all promo copy for this invocation.
if _promos_disabled_by_env() or not interactive:
return

# Trigger: command context where cloud promo is not actionable.
# Why: mcp/stdin protocol and root help flows should stay noise-free.
# Outcome: command continues without promo messaging.
if invoked_subcommand in {None, "mcp"}:
return

if config.cloud_mode_enabled or config.cloud_promo_opt_out:
return

show_first_run = not config.cloud_promo_first_run_shown
show_version_notice = config.cloud_promo_last_version_shown != CLOUD_PROMO_VERSION
if not show_first_run and not show_version_notice:
return

message = _build_first_run_message() if show_first_run else _build_version_message()
echo(message)

config.cloud_promo_first_run_shown = True
config.cloud_promo_last_version_shown = CLOUD_PROMO_VERSION
manager.save_config(config)
15 changes: 15 additions & 0 deletions src/basic_memory/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,21 @@ class BasicMemoryConfig(BaseSettings):
description="Cloud project sync configuration mapping project names to their local paths and sync state",
)

cloud_promo_opt_out: bool = Field(
default=False,
description="Disable CLI cloud promo messages when true.",
)

cloud_promo_first_run_shown: bool = Field(
default=False,
description="Tracks whether the first-run cloud promo message has been shown.",
)

cloud_promo_last_version_shown: Optional[str] = Field(
default=None,
description="Most recent cloud promo version shown in CLI.",
)

@property
def is_test_env(self) -> bool:
"""Check if running in a test environment.
Expand Down
16 changes: 16 additions & 0 deletions src/basic_memory/mcp/resources/cloud_info.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Basic Memory Cloud (optional)

Basic Memory Cloud is an optional add-on for users who want hosted access and sync.

- Hosted access to your knowledge
- Cross-device sync
- Mobile and web access
- Multi-client workflows (Claude, ChatGPT, Gemini, and others)

OSS discount: `{{OSS_DISCOUNT_CODE}}` (20% off for 3 months)

Get started:

```bash
bm cloud login
```
16 changes: 16 additions & 0 deletions src/basic_memory/mcp/resources/release_notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Release Notes

## 2026-02-06

- Added optional cloud discovery copy to CLI first-run and promo-version notices.
- Added MCP tools for opt-in cloud discovery: `cloud_info` and `release_notes`.
- Updated docs and README copy to keep cloud messaging explicit and optional.

Cloud remains optional for open-source users.
OSS discount: `{{OSS_DISCOUNT_CODE}}` (20% off for 3 months)

Get started:

```bash
bm cloud login
```
4 changes: 4 additions & 0 deletions src/basic_memory/mcp/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from basic_memory.mcp.tools.read_note import read_note
from basic_memory.mcp.tools.view_note import view_note
from basic_memory.mcp.tools.write_note import write_note
from basic_memory.mcp.tools.cloud_info import cloud_info
from basic_memory.mcp.tools.release_notes import release_notes
from basic_memory.mcp.tools.search import search_notes, search_by_metadata
from basic_memory.mcp.tools.canvas import canvas
from basic_memory.mcp.tools.list_directory import list_directory
Expand All @@ -30,6 +32,7 @@
__all__ = [
"build_context",
"canvas",
"cloud_info",
"create_memory_project",
"delete_note",
"delete_project",
Expand All @@ -40,6 +43,7 @@
"move_note",
"read_content",
"read_note",
"release_notes",
"recent_activity",
"search",
"search_by_metadata",
Expand Down
12 changes: 12 additions & 0 deletions src/basic_memory/mcp/tools/cloud_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Cloud information MCP tool."""

from pathlib import Path

from basic_memory.mcp.server import mcp


@mcp.tool("cloud_info")
def cloud_info() -> str:
"""Return optional Basic Memory Cloud information and setup guidance."""
content_path = Path(__file__).parent.parent / "resources" / "cloud_info.md"
return content_path.read_text(encoding="utf-8")
12 changes: 12 additions & 0 deletions src/basic_memory/mcp/tools/release_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Release notes MCP tool."""

from pathlib import Path

from basic_memory.mcp.server import mcp


@mcp.tool("release_notes")
def release_notes() -> str:
"""Return the latest product release notes for optional user review."""
content_path = Path(__file__).parent.parent / "resources" / "release_notes.md"
return content_path.read_text(encoding="utf-8")
Loading
Loading