diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 444bbe4a8..c126aafea 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -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"' - diff --git a/src/basic_memory/__init__.py b/src/basic_memory/__init__.py index 34c8178e9..02406d79f 100644 --- a/src/basic_memory/__init__.py +++ b/src/basic_memory/__init__.py @@ -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" diff --git a/src/basic_memory/cli/auth.py b/src/basic_memory/cli/auth.py index 949bd236d..7a746bf4e 100644 --- a/src/basic_memory/cli/auth.py +++ b/src/basic_memory/cli/auth.py @@ -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]") @@ -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.""" @@ -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]") @@ -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]") diff --git a/src/basic_memory/cli/commands/cloud/core_commands.py b/src/basic_memory/cli/commands/cloud/core_commands.py index 2d95a670c..183573e74 100644 --- a/src/basic_memory/cli/commands/cloud/core_commands.py +++ b/src/basic_memory/cli/commands/cloud/core_commands.py @@ -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( @@ -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]") @@ -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]") @@ -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") diff --git a/src/basic_memory/cli/commands/cloud/rclone_config.py b/src/basic_memory/cli/commands/cloud/rclone_config.py index f343f4e77..50edb8783 100644 --- a/src/basic_memory/cli/commands/cloud/rclone_config.py +++ b/src/basic_memory/cli/commands/cloud/rclone_config.py @@ -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 diff --git a/src/basic_memory/cli/commands/cloud/rclone_installer.py b/src/basic_memory/cli/commands/cloud/rclone_installer.py index 8b6e20062..77cc24ad8 100644 --- a/src/basic_memory/cli/commands/cloud/rclone_installer.py +++ b/src/basic_memory/cli/commands/cloud/rclone_installer.py @@ -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( @@ -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" @@ -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]") @@ -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]") @@ -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" @@ -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]") @@ -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]") @@ -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]") @@ -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 diff --git a/src/basic_memory/cli/commands/cloud/upload.py b/src/basic_memory/cli/commands/cloud/upload.py index deb71673d..517dea107 100644 --- a/src/basic_memory/cli/commands/cloud/upload.py +++ b/src/basic_memory/cli/commands/cloud/upload.py @@ -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 diff --git a/src/basic_memory/cli/commands/cloud/upload_command.py b/src/basic_memory/cli/commands/cloud/upload_command.py index 11d9e556a..3e7795274 100644 --- a/src/basic_memory/cli/commands/cloud/upload_command.py +++ b/src/basic_memory/cli/commands/cloud/upload_command.py @@ -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) @@ -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 diff --git a/src/basic_memory/cli/commands/command_utils.py b/src/basic_memory/cli/commands/command_utils.py index 9abb6774f..7f2515160 100644 --- a/src/basic_memory/cli/commands/command_utils.py +++ b/src/basic_memory/cli/commands/command_utils.py @@ -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) @@ -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) diff --git a/src/basic_memory/cli/commands/project.py b/src/basic_memory/cli/commands/project.py index 2bc8a651a..c15347ba9 100644 --- a/src/basic_memory/cli/commands/project.py +++ b/src/basic_memory/cli/commands/project.py @@ -78,7 +78,7 @@ async def _list_projects(): table.add_column("Default", style="magenta") for project in result.projects: - is_default = "✓" if project.is_default else "" + is_default = "[X]" if project.is_default else "" normalized_path = normalize_project_path(project.path) # Build row based on mode @@ -179,7 +179,7 @@ async def _add_project(): ) ConfigManager().save_config(config) - console.print(f"\n[green]✓ Local sync path configured: {local_sync_path}[/green]") + console.print(f"\n[green]Local sync path configured: {local_sync_path}[/green]") console.print("\nNext steps:") console.print(f" 1. Preview: bm project bisync --name {name} --resync --dry-run") console.print(f" 2. Sync: bm project bisync --name {name} --resync") @@ -233,7 +233,7 @@ async def _verify_project_exists(): ) config_manager.save_config(config) - console.print(f"[green]✓ Sync configured for project '{name}'[/green]") + console.print(f"[green]Sync configured for project '{name}'[/green]") console.print(f"\nLocal sync path: {resolved_path}") console.print("\nNext steps:") console.print(f" 1. Preview: bm project bisync --name {name} --resync --dry-run") @@ -286,7 +286,7 @@ async def _remove_project(): import shutil shutil.rmtree(local_dir) - console.print(f"[green]✓ Removed local sync directory: {local_path}[/green]") + console.print(f"[green]Removed local sync directory: {local_path}[/green]") # Clean up bisync state if it exists if has_bisync_state: @@ -296,7 +296,7 @@ async def _remove_project(): bisync_state_path = get_project_bisync_state(name) if bisync_state_path.exists(): shutil.rmtree(bisync_state_path) - console.print("[green]✓ Removed bisync state[/green]") + console.print("[green]Removed bisync state[/green]") # Clean up cloud_projects config entry if config.cloud_mode_enabled and name in config.cloud_projects: @@ -407,7 +407,7 @@ async def _move_project(): "[yellow]You must manually move your project files from the old location to:[/yellow]\n" f"[cyan]{resolved_path}[/cyan]\n\n" "[dim]Basic Memory has only updated the configuration - your files remain in their original location.[/dim]", - title="⚠️ Manual File Movement Required", + title="Manual File Movement Required", border_style="yellow", expand=False, ) @@ -477,7 +477,7 @@ async def _get_project(): success = project_sync(sync_project, bucket_name, dry_run=dry_run, verbose=verbose) if success: - console.print(f"[green]✓ {name} synced successfully[/green]") + console.print(f"[green]{name} synced successfully[/green]") # Trigger database sync if not a dry run if not dry_run: @@ -494,7 +494,7 @@ async def _trigger_db_sync(): except Exception as e: console.print(f"[yellow]Warning: Could not trigger database sync: {e}[/yellow]") else: - console.print(f"[red]✗ {name} sync failed[/red]") + console.print(f"[red]{name} sync failed[/red]") raise typer.Exit(1) except RcloneError as e: @@ -512,7 +512,7 @@ def bisync_project_command( resync: bool = typer.Option(False, "--resync", help="Force new baseline"), verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"), ) -> None: - """Two-way sync: local ↔ cloud (bidirectional sync). + """Two-way sync: local <-> cloud (bidirectional sync). Examples: bm project bisync --name research --resync # First time @@ -562,13 +562,13 @@ async def _get_project(): ) # Run bisync - console.print(f"[blue]Bisync {name} (local ↔ cloud)...[/blue]") + console.print(f"[blue]Bisync {name} (local <-> cloud)...[/blue]") success = project_bisync( sync_project, bucket_name, dry_run=dry_run, resync=resync, verbose=verbose ) if success: - console.print(f"[green]✓ {name} bisync completed successfully[/green]") + console.print(f"[green]{name} bisync completed successfully[/green]") # Update config config.cloud_projects[name].last_sync = datetime.now() @@ -590,7 +590,7 @@ async def _trigger_db_sync(): except Exception as e: console.print(f"[yellow]Warning: Could not trigger database sync: {e}[/yellow]") else: - console.print(f"[red]✗ {name} bisync failed[/red]") + console.print(f"[red]{name} bisync failed[/red]") raise typer.Exit(1) except RcloneError as e: @@ -658,9 +658,9 @@ async def _get_project(): match = project_check(sync_project, bucket_name, one_way=one_way) if match: - console.print(f"[green]✓ {name} files match[/green]") + console.print(f"[green]{name} files match[/green]") else: - console.print(f"[yellow]⚠ {name} has differences[/yellow]") + console.print(f"[yellow]!{name} has differences[/yellow]") except RcloneError as e: console.print(f"[red]Check error: {e}[/red]") @@ -691,7 +691,7 @@ def bisync_reset( # Remove the entire state directory shutil.rmtree(state_path) - console.print(f"[green]✓ Cleared bisync state for project '{name}'[/green]") + console.print(f"[green]Cleared bisync state for project '{name}'[/green]") console.print("\nNext steps:") console.print(f" 1. Preview: bm project bisync --name {name} --resync --dry-run") console.print(f" 2. Sync: bm project bisync --name {name} --resync") @@ -782,13 +782,13 @@ def display_project_info( f"[bold]Project:[/bold] {info.project_name}\n" f"[bold]Path:[/bold] {info.project_path}\n" f"[bold]Default Project:[/bold] {info.default_project}\n", - title="📊 Basic Memory Project Info", + title="Basic Memory Project Info", expand=False, ) ) # Statistics section - stats_table = Table(title="📈 Statistics") + stats_table = Table(title="Statistics") stats_table.add_column("Metric", style="cyan") stats_table.add_column("Count", style="green") @@ -804,7 +804,7 @@ def display_project_info( # Entity types if info.statistics.entity_types: - entity_types_table = Table(title="📑 Entity Types") + entity_types_table = Table(title="Entity Types") entity_types_table.add_column("Type", style="blue") entity_types_table.add_column("Count", style="green") @@ -815,7 +815,7 @@ def display_project_info( # Most connected entities if info.statistics.most_connected_entities: # pragma: no cover - connected_table = Table(title="🔗 Most Connected Entities") + connected_table = Table(title="Most Connected Entities") connected_table.add_column("Title", style="blue") connected_table.add_column("Permalink", style="cyan") connected_table.add_column("Relations", style="green") @@ -829,7 +829,7 @@ def display_project_info( # Recent activity if info.activity.recently_updated: # pragma: no cover - recent_table = Table(title="🕒 Recent Activity") + recent_table = Table(title="Recent Activity") recent_table.add_column("Title", style="blue") recent_table.add_column("Type", style="cyan") recent_table.add_column("Last Updated", style="green") @@ -849,7 +849,7 @@ def display_project_info( console.print(recent_table) # Available projects - projects_table = Table(title="📁 Available Projects") + projects_table = Table(title="Available Projects") projects_table.add_column("Name", style="blue") projects_table.add_column("Path", style="cyan") projects_table.add_column("Default", style="green") @@ -857,7 +857,7 @@ def display_project_info( for name, proj_info in info.available_projects.items(): is_default = name == info.default_project project_path = proj_info["path"] - projects_table.add_row(name, project_path, "✓" if is_default else "") + projects_table.add_row(name, project_path, "[X]" if is_default else "") console.print(projects_table) diff --git a/src/basic_memory/cli/commands/status.py b/src/basic_memory/cli/commands/status.py index e67e2a042..983b1b6af 100644 --- a/src/basic_memory/cli/commands/status.py +++ b/src/basic_memory/cli/commands/status.py @@ -115,7 +115,7 @@ def display_changes( del_branch = tree.add("[red]Deleted[/red]") add_files_to_tree(del_branch, changes.deleted, "red") if changes.skipped_files: - skip_branch = tree.add("[red]⚠️ Skipped (Circuit Breaker)[/red]") + skip_branch = tree.add("[red]! Skipped (Circuit Breaker)[/red]") for skipped in sorted(changes.skipped_files, key=lambda x: x.path): skip_branch.add( f"[red]{skipped.path}[/red] " @@ -133,7 +133,7 @@ def display_changes( if changes.skipped_files: skip_count = len(changes.skipped_files) tree.add( - f"[red]⚠️ {skip_count} file{'s' if skip_count != 1 else ''} " + f"[red]! {skip_count} file{'s' if skip_count != 1 else ''} " f"skipped due to repeated failures[/red]" ) @@ -152,7 +152,7 @@ async def run_status(project: Optional[str] = None, verbose: bool = False): # p display_changes(project_item.name, "Status", sync_report, verbose) except (ValueError, ToolError) as e: - console.print(f"[red]✗ Error: {e}[/red]") + console.print(f"[red]Error: {e}[/red]") raise typer.Exit(1) diff --git a/test-int/cli/test_project_commands_integration.py b/test-int/cli/test_project_commands_integration.py index 920e52d18..f1df7325f 100644 --- a/test-int/cli/test_project_commands_integration.py +++ b/test-int/cli/test_project_commands_integration.py @@ -19,7 +19,7 @@ def test_project_list(app_config, test_project, config_manager): print(f"Exception: {result.exception}") assert result.exit_code == 0 assert "test-project" in result.stdout - assert "✓" in result.stdout # default marker + assert "[X]" in result.stdout # default marker def test_project_info(app_config, test_project, config_manager): @@ -114,8 +114,8 @@ def test_project_set_default(app_config, config_manager): # Verify in list result = runner.invoke(app, ["project", "list"]) assert result.exit_code == 0 - # The new project should have the checkmark now + # The new project should have the [X] marker now lines = result.stdout.split("\n") for line in lines: if "another-project" in line: - assert "✓" in line + assert "[X]" in line diff --git a/tests/schemas/test_schemas.py b/tests/schemas/test_schemas.py index ca1b9383c..ed7377f0d 100644 --- a/tests/schemas/test_schemas.py +++ b/tests/schemas/test_schemas.py @@ -348,7 +348,7 @@ def test_parse_timeframe_other_formats(self): result_1d = parse_timeframe("1d") expected_1d = now - timedelta(days=1) diff = abs((result_1d - expected_1d).total_seconds()) - assert diff < 60 # Within 1 minute tolerance + assert diff < 3600 # Within 1 hour tolerance (accounts for DST transitions) assert result_1d.tzinfo is not None # Test yesterday - should be yesterday at same time diff --git a/tests/sync/test_sync_service_incremental.py b/tests/sync/test_sync_service_incremental.py index 6650e2238..854f58e31 100644 --- a/tests/sync/test_sync_service_incremental.py +++ b/tests/sync/test_sync_service_incremental.py @@ -179,12 +179,13 @@ async def test_force_full_bypasses_watermark_optimization( # Modify a file WITHOUT updating mtime (simulates external tool like rclone) # We set mtime to be BEFORE the watermark to ensure incremental scan won't detect it file_path = project_dir / "file1.md" - original_stat = file_path.stat() + file_path.stat() await create_test_file(file_path, "# File 1\nModified by external tool") # Set mtime to be before the watermark (use time from before first sync) # This simulates rclone bisync which may preserve original timestamps import os + old_time = initial_timestamp - 10 # 10 seconds before watermark os.utime(file_path, (old_time, old_time))