Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a927419
Add SPEC-20: Simplified Project-Scoped Rclone Sync
phernandez Oct 28, 2025
8bc550f
feat(config): Add cloud_projects schema for project-scoped sync (SPEC…
phernandez Oct 28, 2025
7c2b8b5
feat(rclone): Add simplified configure_rclone_remote() function (SPEC…
phernandez Oct 28, 2025
0887ad4
refactor(rclone): Clean up deprecated functions from rclone_config.py
phernandez Oct 28, 2025
685cccf
refactor: Clean up deprecated rclone_config import references
phernandez Oct 28, 2025
7306524
feat: Add project-scoped rclone commands (SPEC-20 Phase 3)
phernandez Oct 28, 2025
ffe5aa4
docs: Update SPEC-20 to mark Phase 2 and Phase 3 complete
phernandez Oct 28, 2025
3b1cd87
feat: Add project sync CLI commands (SPEC-20 Phase 4)
phernandez Oct 28, 2025
c63bf1a
docs: Mark SPEC-20 Phase 4 complete
phernandez Oct 28, 2025
22d1a8b
feat(SPEC-20): Phase 5 cleanup - Remove tenant-wide sync operations
phernandez Oct 28, 2025
daf5add
docs(SPEC-20): Mark Phase 5 cleanup as complete
phernandez Oct 28, 2025
da9c702
docs(SPEC-20): Update cloud-cli.md for project-scoped sync
phernandez Oct 28, 2025
db85186
fix: Remove obsolete tests for deleted sync functionality
phernandez Oct 28, 2025
d749c77
feat: SPEC-20 enhancements - cleanup, path normalization, and docs
phernandez Oct 28, 2025
b049c5c
test: fix rclone command tests for path normalization
phernandez Oct 28, 2025
bc37ecf
fix: handle project removal for cloud-only projects
phernandez Oct 28, 2025
d754cf9
fix: use cross-platform path comparisons in Windows tests
phernandez Oct 28, 2025
045e931
fix: use Path comparison for cross-platform rclone command test
phernandez Oct 30, 2025
e114de9
fix: configure rclone encoding to prevent quoting filenames with spaces
phernandez Nov 1, 2025
0c29884
feat: add force_full parameter to sync API for bisync operations
phernandez Nov 1, 2025
6c720da
refactor: consolidate normalize_project_path into shared utils module
phernandez Nov 1, 2025
895a799
test: add tests for force_full parameter and fix Windows path normali…
phernandez Nov 1, 2025
36149d6
fix: improve force_full test to set mtime before watermark
phernandez Nov 1, 2025
92bd3a5
Fix Windows CLI Unicode encoding errors
groksrc Nov 2, 2025
5642ac9
Fix Windows Unicode error with bidirectional arrow character
groksrc Nov 2, 2025
e75aa0b
Merge branch 'main' into fix/windows-cli-unicode-support
phernandez Nov 2, 2025
af835c3
add unicode rule to code review
phernandez Nov 2, 2025
7fdcfda
fix unicode assertions in tests
phernandez Nov 2, 2025
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
5 changes: 4 additions & 1 deletion .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,12 @@ jobs:
- [ ] Proper error handling and logging
- [ ] Performance considerations addressed
- [ ] No sensitive data in logs or commits

## Compatability
- [ ] File path comparisons must be windows compatible
- [ ] Avoid using emojis and unicode characters in console and log output

Read the CLAUDE.md file for detailed project context. For each checklist item, verify if it's satisfied and comment on any that need attention. Use inline comments for specific code issues and post a summary with checklist results.

# Allow broader tool access for thorough code review
claude_args: '--allowed-tools "Bash(gh pr:*),Bash(gh issue:*),Bash(gh api:*),Bash(git log:*),Bash(git show:*),Read,Grep,Glob"'

2 changes: 1 addition & 1 deletion src/basic_memory/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""

# Package version - updated by release automation
__version__ = "0.15.2"
__version__ = "0.15.3.dev42+36149d6"

# API version for FastAPI - independent of package version
__api_version__ = "v0"
10 changes: 5 additions & 5 deletions src/basic_memory/cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def display_user_instructions(self, device_response: dict) -> None:
verification_uri = device_response["verification_uri"]
verification_uri_complete = device_response.get("verification_uri_complete")

console.print("\n[bold blue]🔐 Authentication Required[/bold blue]")
console.print("\n[bold blue]Authentication Required[/bold blue]")
console.print("\nTo authenticate, please visit:")
console.print(f"[bold cyan]{verification_uri}[/bold cyan]")
console.print(f"\nAnd enter this code: [bold yellow]{user_code}[/bold yellow]")
Expand Down Expand Up @@ -171,7 +171,7 @@ def save_tokens(self, tokens: dict) -> None:
# Secure the token file
os.chmod(self.token_file, 0o600)

console.print(f"[green]Tokens saved to {self.token_file}[/green]")
console.print(f"[green]Tokens saved to {self.token_file}[/green]")

def load_tokens(self) -> dict | None:
"""Load tokens from .bm-auth.json file."""
Expand Down Expand Up @@ -233,7 +233,7 @@ async def get_valid_token(self) -> str | None:
if new_tokens:
# Save new tokens (may include rotated refresh token)
self.save_tokens(new_tokens)
console.print("[green]Token refreshed successfully[/green]")
console.print("[green]Token refreshed successfully[/green]")
return new_tokens["access_token"]
else:
console.print("[yellow]Token refresh failed. Please run 'login' again.[/yellow]")
Expand Down Expand Up @@ -265,13 +265,13 @@ async def login(self) -> bool:
# Step 4: Save tokens
self.save_tokens(tokens)

console.print("\n[green]Successfully authenticated with Basic Memory Cloud![/green]")
console.print("\n[green]Successfully authenticated with Basic Memory Cloud![/green]")
return True

def logout(self) -> None:
"""Remove stored authentication tokens."""
if self.token_file.exists():
self.token_file.unlink()
console.print("[green]Logged out successfully[/green]")
console.print("[green]Logged out successfully[/green]")
else:
console.print("[yellow]No stored authentication found[/yellow]")
12 changes: 6 additions & 6 deletions src/basic_memory/cli/commands/cloud/core_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ async def _login():
config.cloud_mode = True
config_manager.save_config(config)

console.print("[green]Cloud mode enabled[/green]")
console.print("[green]Cloud mode enabled[/green]")
console.print(f"[dim]All CLI commands now work against {host_url}[/dim]")

except SubscriptionRequiredError as e:
console.print("\n[red]Subscription Required[/red]\n")
console.print("\n[red]Subscription Required[/red]\n")
console.print(f"[yellow]{e.args[0]}[/yellow]\n")
console.print(f"Subscribe at: [blue underline]{e.subscribe_url}[/blue underline]\n")
console.print(
Expand All @@ -77,7 +77,7 @@ def logout():
config.cloud_mode = False
config_manager.save_config(config)

console.print("[green]Cloud mode disabled[/green]")
console.print("[green]Cloud mode disabled[/green]")
console.print("[dim]All CLI commands now work locally[/dim]")


Expand Down Expand Up @@ -157,12 +157,12 @@ def setup() -> None:
# Step 2: Get tenant info
console.print("\n[blue]Step 2: Getting tenant information...[/blue]")
tenant_info = asyncio.run(get_mount_info())
console.print(f"[green]Found tenant: {tenant_info.tenant_id}[/green]")
console.print(f"[green]Found tenant: {tenant_info.tenant_id}[/green]")

# Step 3: Generate credentials
console.print("\n[blue]Step 3: Generating sync credentials...[/blue]")
creds = asyncio.run(generate_mount_credentials(tenant_info.tenant_id))
console.print("[green]Generated secure credentials[/green]")
console.print("[green]Generated secure credentials[/green]")

# Step 4: Configure rclone remote
console.print("\n[blue]Step 4: Configuring rclone remote...[/blue]")
Expand All @@ -171,7 +171,7 @@ def setup() -> None:
secret_key=creds.secret_key,
)

console.print("\n[bold green]Cloud setup completed successfully![/bold green]")
console.print("\n[bold green]Cloud setup completed successfully![/bold green]")
console.print("\n[bold]Next steps:[/bold]")
console.print("1. Add a project with local sync path:")
console.print(" bm project add research --local-path ~/Documents/research")
Expand Down
2 changes: 1 addition & 1 deletion src/basic_memory/cli/commands/cloud/rclone_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,5 @@ def configure_rclone_remote(
# Save updated config
save_rclone_config(config)

console.print(f"[green]Configured rclone remote: {REMOTE_NAME}[/green]")
console.print(f"[green]Configured rclone remote: {REMOTE_NAME}[/green]")
return REMOTE_NAME
18 changes: 9 additions & 9 deletions src/basic_memory/cli/commands/cloud/rclone_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def install_rclone_macos() -> None:
try:
console.print("[blue]Installing rclone via Homebrew...[/blue]")
run_command(["brew", "install", "rclone"])
console.print("[green]rclone installed via Homebrew[/green]")
console.print("[green]rclone installed via Homebrew[/green]")
return
except RcloneInstallError:
console.print(
Expand All @@ -69,7 +69,7 @@ def install_rclone_macos() -> None:
console.print("[blue]Installing rclone via official script...[/blue]")
try:
run_command(["sh", "-c", "curl https://rclone.org/install.sh | sudo bash"])
console.print("[green]rclone installed via official script[/green]")
console.print("[green]rclone installed via official script[/green]")
except RcloneInstallError:
raise RcloneInstallError(
"Failed to install rclone. Please install manually: brew install rclone"
Expand All @@ -83,7 +83,7 @@ def install_rclone_linux() -> None:
try:
console.print("[blue]Installing rclone via snap...[/blue]")
run_command(["sudo", "snap", "install", "rclone"])
console.print("[green]rclone installed via snap[/green]")
console.print("[green]rclone installed via snap[/green]")
return
except RcloneInstallError:
console.print("[yellow]Snap installation failed, trying apt...[/yellow]")
Expand All @@ -94,7 +94,7 @@ def install_rclone_linux() -> None:
console.print("[blue]Installing rclone via apt...[/blue]")
run_command(["sudo", "apt", "update"])
run_command(["sudo", "apt", "install", "-y", "rclone"])
console.print("[green]rclone installed via apt[/green]")
console.print("[green]rclone installed via apt[/green]")
return
except RcloneInstallError:
console.print("[yellow]apt installation failed, trying official script...[/yellow]")
Expand All @@ -103,7 +103,7 @@ def install_rclone_linux() -> None:
console.print("[blue]Installing rclone via official script...[/blue]")
try:
run_command(["sh", "-c", "curl https://rclone.org/install.sh | sudo bash"])
console.print("[green]rclone installed via official script[/green]")
console.print("[green]rclone installed via official script[/green]")
except RcloneInstallError:
raise RcloneInstallError(
"Failed to install rclone. Please install manually: sudo snap install rclone"
Expand All @@ -117,7 +117,7 @@ def install_rclone_windows() -> None:
try:
console.print("[blue]Installing rclone via winget...[/blue]")
run_command(["winget", "install", "Rclone.Rclone"])
console.print("[green]rclone installed via winget[/green]")
console.print("[green]rclone installed via winget[/green]")
return
except RcloneInstallError:
console.print("[yellow]winget installation failed, trying chocolatey...[/yellow]")
Expand All @@ -127,7 +127,7 @@ def install_rclone_windows() -> None:
try:
console.print("[blue]Installing rclone via chocolatey...[/blue]")
run_command(["choco", "install", "rclone", "-y"])
console.print("[green]rclone installed via chocolatey[/green]")
console.print("[green]rclone installed via chocolatey[/green]")
return
except RcloneInstallError:
console.print("[yellow]chocolatey installation failed, trying scoop...[/yellow]")
Expand All @@ -137,7 +137,7 @@ def install_rclone_windows() -> None:
try:
console.print("[blue]Installing rclone via scoop...[/blue]")
run_command(["scoop", "install", "rclone"])
console.print("[green]rclone installed via scoop[/green]")
console.print("[green]rclone installed via scoop[/green]")
return
except RcloneInstallError:
console.print("[yellow]scoop installation failed[/yellow]")
Expand Down Expand Up @@ -172,7 +172,7 @@ def install_rclone(platform_override: Optional[str] = None) -> None:
if not is_rclone_installed():
raise RcloneInstallError("rclone installation completed but command not found in PATH")

console.print("[green]rclone installation completed successfully[/green]")
console.print("[green]rclone installation completed successfully[/green]")

except RcloneInstallError:
raise
Expand Down
2 changes: 1 addition & 1 deletion src/basic_memory/cli/commands/cloud/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ async def upload_path(
if dry_run:
print(f"\nTotal: {len(files_to_upload)} file(s) ({size_str})")
else:
print(f"Upload complete: {len(files_to_upload)} file(s) ({size_str})")
print(f"Upload complete: {len(files_to_upload)} file(s) ({size_str})")

return True

Expand Down
4 changes: 2 additions & 2 deletions src/basic_memory/cli/commands/cloud/upload_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ async def _upload():
console.print(f"[blue]Creating cloud project '{project}'...[/blue]")
try:
await create_cloud_project(project)
console.print(f"[green]Created project '{project}'[/green]")
console.print(f"[green]Created project '{project}'[/green]")
except Exception as e:
console.print(f"[red]Failed to create project: {e}[/red]")
raise typer.Exit(1)
Expand Down Expand Up @@ -109,7 +109,7 @@ async def _upload():
if dry_run:
console.print("[yellow]DRY RUN complete - no files were uploaded[/yellow]")
else:
console.print(f"[green]Successfully uploaded to '{project}'[/green]")
console.print(f"[green]Successfully uploaded to '{project}'[/green]")

# Sync project if requested (skip on dry run)
# Force full scan after bisync to ensure database is up-to-date with synced files
Expand Down
6 changes: 3 additions & 3 deletions src/basic_memory/cli/commands/command_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ async def run_sync(project: Optional[str] = None, force_full: bool = False):
url += "?force_full=true"
response = await call_post(client, url)
data = response.json()
console.print(f"[green]{data['message']}[/green]")
console.print(f"[green]{data['message']}[/green]")
except (ToolError, ValueError) as e:
console.print(f"[red]Sync failed: {e}[/red]")
console.print(f"[red]Sync failed: {e}[/red]")
raise typer.Exit(1)


Expand All @@ -47,5 +47,5 @@ async def get_project_info(project: str):
response = await call_get(client, f"{project_item.project_url}/project/info")
return ProjectInfoResponse.model_validate(response.json())
except (ToolError, ValueError) as e:
console.print(f"[red]Sync failed: {e}[/red]")
console.print(f"[red]Sync failed: {e}[/red]")
raise typer.Exit(1)
Loading
Loading