basic-memory 0.14.3__py3-none-any.whl → 0.15.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of basic-memory might be problematic. Click here for more details.

Files changed (90) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  3. basic_memory/api/app.py +10 -4
  4. basic_memory/api/routers/knowledge_router.py +25 -8
  5. basic_memory/api/routers/project_router.py +99 -4
  6. basic_memory/api/routers/resource_router.py +3 -3
  7. basic_memory/cli/app.py +9 -28
  8. basic_memory/cli/auth.py +277 -0
  9. basic_memory/cli/commands/cloud/__init__.py +5 -0
  10. basic_memory/cli/commands/cloud/api_client.py +112 -0
  11. basic_memory/cli/commands/cloud/bisync_commands.py +818 -0
  12. basic_memory/cli/commands/cloud/core_commands.py +288 -0
  13. basic_memory/cli/commands/cloud/mount_commands.py +295 -0
  14. basic_memory/cli/commands/cloud/rclone_config.py +288 -0
  15. basic_memory/cli/commands/cloud/rclone_installer.py +198 -0
  16. basic_memory/cli/commands/command_utils.py +60 -0
  17. basic_memory/cli/commands/import_memory_json.py +0 -4
  18. basic_memory/cli/commands/mcp.py +16 -4
  19. basic_memory/cli/commands/project.py +141 -145
  20. basic_memory/cli/commands/status.py +34 -22
  21. basic_memory/cli/commands/sync.py +45 -228
  22. basic_memory/cli/commands/tool.py +87 -16
  23. basic_memory/cli/main.py +1 -0
  24. basic_memory/config.py +96 -20
  25. basic_memory/db.py +104 -3
  26. basic_memory/deps.py +20 -3
  27. basic_memory/file_utils.py +89 -0
  28. basic_memory/ignore_utils.py +295 -0
  29. basic_memory/importers/chatgpt_importer.py +1 -1
  30. basic_memory/importers/utils.py +2 -2
  31. basic_memory/markdown/entity_parser.py +2 -2
  32. basic_memory/markdown/markdown_processor.py +2 -2
  33. basic_memory/markdown/plugins.py +39 -21
  34. basic_memory/markdown/utils.py +1 -1
  35. basic_memory/mcp/async_client.py +22 -10
  36. basic_memory/mcp/project_context.py +141 -0
  37. basic_memory/mcp/prompts/ai_assistant_guide.py +49 -4
  38. basic_memory/mcp/prompts/continue_conversation.py +1 -1
  39. basic_memory/mcp/prompts/recent_activity.py +116 -32
  40. basic_memory/mcp/prompts/search.py +1 -1
  41. basic_memory/mcp/prompts/utils.py +11 -4
  42. basic_memory/mcp/resources/ai_assistant_guide.md +179 -41
  43. basic_memory/mcp/resources/project_info.py +20 -6
  44. basic_memory/mcp/server.py +0 -37
  45. basic_memory/mcp/tools/__init__.py +5 -6
  46. basic_memory/mcp/tools/build_context.py +39 -19
  47. basic_memory/mcp/tools/canvas.py +19 -8
  48. basic_memory/mcp/tools/chatgpt_tools.py +178 -0
  49. basic_memory/mcp/tools/delete_note.py +67 -34
  50. basic_memory/mcp/tools/edit_note.py +55 -39
  51. basic_memory/mcp/tools/headers.py +44 -0
  52. basic_memory/mcp/tools/list_directory.py +18 -8
  53. basic_memory/mcp/tools/move_note.py +119 -41
  54. basic_memory/mcp/tools/project_management.py +77 -229
  55. basic_memory/mcp/tools/read_content.py +28 -12
  56. basic_memory/mcp/tools/read_note.py +97 -57
  57. basic_memory/mcp/tools/recent_activity.py +441 -42
  58. basic_memory/mcp/tools/search.py +82 -70
  59. basic_memory/mcp/tools/sync_status.py +5 -4
  60. basic_memory/mcp/tools/utils.py +19 -0
  61. basic_memory/mcp/tools/view_note.py +31 -6
  62. basic_memory/mcp/tools/write_note.py +65 -14
  63. basic_memory/models/knowledge.py +19 -2
  64. basic_memory/models/project.py +6 -2
  65. basic_memory/repository/entity_repository.py +31 -84
  66. basic_memory/repository/project_repository.py +1 -1
  67. basic_memory/repository/relation_repository.py +13 -0
  68. basic_memory/repository/repository.py +2 -2
  69. basic_memory/repository/search_repository.py +9 -3
  70. basic_memory/schemas/__init__.py +6 -0
  71. basic_memory/schemas/base.py +70 -12
  72. basic_memory/schemas/cloud.py +46 -0
  73. basic_memory/schemas/memory.py +99 -18
  74. basic_memory/schemas/project_info.py +9 -10
  75. basic_memory/schemas/sync_report.py +48 -0
  76. basic_memory/services/context_service.py +35 -11
  77. basic_memory/services/directory_service.py +7 -0
  78. basic_memory/services/entity_service.py +82 -52
  79. basic_memory/services/initialization.py +30 -11
  80. basic_memory/services/project_service.py +23 -33
  81. basic_memory/sync/sync_service.py +148 -24
  82. basic_memory/sync/watch_service.py +128 -44
  83. basic_memory/utils.py +181 -109
  84. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/METADATA +26 -96
  85. basic_memory-0.15.0.dist-info/RECORD +147 -0
  86. basic_memory/mcp/project_session.py +0 -120
  87. basic_memory-0.14.3.dist-info/RECORD +0 -132
  88. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/WHEEL +0 -0
  89. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/entry_points.txt +0 -0
  90. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,288 @@
1
+ """Core cloud commands for Basic Memory CLI."""
2
+
3
+ import asyncio
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from rich.console import Console
8
+
9
+ from basic_memory.cli.app import cloud_app
10
+ from basic_memory.cli.auth import CLIAuth
11
+ from basic_memory.config import ConfigManager
12
+ from basic_memory.cli.commands.cloud.api_client import (
13
+ CloudAPIError,
14
+ SubscriptionRequiredError,
15
+ get_cloud_config,
16
+ make_api_request,
17
+ )
18
+ from basic_memory.cli.commands.cloud.mount_commands import (
19
+ mount_cloud_files,
20
+ setup_cloud_mount,
21
+ show_mount_status,
22
+ unmount_cloud_files,
23
+ )
24
+ from basic_memory.cli.commands.cloud.bisync_commands import (
25
+ run_bisync,
26
+ run_bisync_watch,
27
+ run_check,
28
+ setup_cloud_bisync,
29
+ show_bisync_status,
30
+ )
31
+ from basic_memory.cli.commands.cloud.rclone_config import MOUNT_PROFILES
32
+ from basic_memory.cli.commands.cloud.bisync_commands import BISYNC_PROFILES
33
+
34
+ console = Console()
35
+
36
+
37
+ @cloud_app.command()
38
+ def login():
39
+ """Authenticate with WorkOS using OAuth Device Authorization flow and enable cloud mode."""
40
+
41
+ async def _login():
42
+ client_id, domain, host_url = get_cloud_config()
43
+ auth = CLIAuth(client_id=client_id, authkit_domain=domain)
44
+
45
+ try:
46
+ success = await auth.login()
47
+ if not success:
48
+ console.print("[red]Login failed[/red]")
49
+ raise typer.Exit(1)
50
+
51
+ # Test subscription access by calling a protected endpoint
52
+ console.print("[dim]Verifying subscription access...[/dim]")
53
+ await make_api_request("GET", f"{host_url.rstrip('/')}/proxy/health")
54
+
55
+ # Enable cloud mode after successful login and subscription validation
56
+ config_manager = ConfigManager()
57
+ config = config_manager.load_config()
58
+ config.cloud_mode = True
59
+ config_manager.save_config(config)
60
+
61
+ console.print("[green]✓ Cloud mode enabled[/green]")
62
+ console.print(f"[dim]All CLI commands now work against {host_url}[/dim]")
63
+
64
+ except SubscriptionRequiredError as e:
65
+ console.print("\n[red]✗ Subscription Required[/red]\n")
66
+ console.print(f"[yellow]{e.args[0]}[/yellow]\n")
67
+ console.print(f"Subscribe at: [blue underline]{e.subscribe_url}[/blue underline]\n")
68
+ console.print(
69
+ "[dim]Once you have an active subscription, run [bold]bm cloud login[/bold] again.[/dim]"
70
+ )
71
+ raise typer.Exit(1)
72
+
73
+ asyncio.run(_login())
74
+
75
+
76
+ @cloud_app.command()
77
+ def logout():
78
+ """Disable cloud mode and return to local mode."""
79
+
80
+ # Disable cloud mode
81
+ config_manager = ConfigManager()
82
+ config = config_manager.load_config()
83
+ config.cloud_mode = False
84
+ config_manager.save_config(config)
85
+
86
+ console.print("[green]✓ Cloud mode disabled[/green]")
87
+ console.print("[dim]All CLI commands now work locally[/dim]")
88
+
89
+
90
+ @cloud_app.command("status")
91
+ def status(
92
+ bisync: bool = typer.Option(
93
+ True,
94
+ "--bisync/--mount",
95
+ help="Show bisync status (default) or mount status",
96
+ ),
97
+ ) -> None:
98
+ """Check cloud mode status and cloud instance health.
99
+
100
+ Shows cloud mode status, instance health, and sync/mount status.
101
+ Use --bisync (default) to show bisync status or --mount for mount status.
102
+ """
103
+ # Check cloud mode
104
+ config_manager = ConfigManager()
105
+ config = config_manager.load_config()
106
+
107
+ console.print("[bold blue]Cloud Mode Status[/bold blue]")
108
+ if config.cloud_mode:
109
+ console.print(" Mode: [green]Cloud (enabled)[/green]")
110
+ console.print(f" Host: {config.cloud_host}")
111
+ console.print(" [dim]All CLI commands work against cloud[/dim]")
112
+ else:
113
+ console.print(" Mode: [yellow]Local (disabled)[/yellow]")
114
+ console.print(" [dim]All CLI commands work locally[/dim]")
115
+ console.print("\n[dim]To enable cloud mode, run: bm cloud login[/dim]")
116
+ return
117
+
118
+ # Get cloud configuration
119
+ _, _, host_url = get_cloud_config()
120
+ host_url = host_url.rstrip("/")
121
+
122
+ # Prepare headers
123
+ headers = {}
124
+
125
+ try:
126
+ console.print("\n[blue]Checking cloud instance health...[/blue]")
127
+
128
+ # Make API request to check health
129
+ response = asyncio.run(
130
+ make_api_request(method="GET", url=f"{host_url}/proxy/health", headers=headers)
131
+ )
132
+
133
+ health_data = response.json()
134
+
135
+ console.print("[green]Cloud instance is healthy[/green]")
136
+
137
+ # Display status details
138
+ if "status" in health_data:
139
+ console.print(f" Status: {health_data['status']}")
140
+ if "version" in health_data:
141
+ console.print(f" Version: {health_data['version']}")
142
+ if "timestamp" in health_data:
143
+ console.print(f" Timestamp: {health_data['timestamp']}")
144
+
145
+ # Show sync/mount status based on flag
146
+ console.print()
147
+ if bisync:
148
+ show_bisync_status()
149
+ else:
150
+ show_mount_status()
151
+
152
+ except CloudAPIError as e:
153
+ console.print(f"[red]Error checking cloud health: {e}[/red]")
154
+ raise typer.Exit(1)
155
+ except Exception as e:
156
+ console.print(f"[red]Unexpected error: {e}[/red]")
157
+ raise typer.Exit(1)
158
+
159
+
160
+ # Mount commands
161
+
162
+
163
+ @cloud_app.command("setup")
164
+ def setup(
165
+ bisync: bool = typer.Option(
166
+ True,
167
+ "--bisync/--mount",
168
+ help="Use bidirectional sync (recommended) or mount as network drive",
169
+ ),
170
+ sync_dir: Optional[str] = typer.Option(
171
+ None,
172
+ "--dir",
173
+ help="Custom sync directory for bisync (default: ~/basic-memory-cloud-sync)",
174
+ ),
175
+ ) -> None:
176
+ """Set up cloud file access with automatic rclone installation and configuration.
177
+
178
+ Default: Sets up bidirectional sync (recommended).\n
179
+ Use --mount: Sets up mount as network drive (alternative workflow).\n
180
+
181
+ Examples:\n
182
+ bm cloud setup # Setup bisync (default)\n
183
+ bm cloud setup --mount # Setup mount instead\n
184
+ bm cloud setup --dir ~/sync # Custom bisync directory\n
185
+ """
186
+ if bisync:
187
+ setup_cloud_bisync(sync_dir=sync_dir)
188
+ else:
189
+ setup_cloud_mount()
190
+
191
+
192
+ @cloud_app.command("mount")
193
+ def mount(
194
+ profile: str = typer.Option(
195
+ "balanced", help=f"Mount profile: {', '.join(MOUNT_PROFILES.keys())}"
196
+ ),
197
+ path: Optional[str] = typer.Option(
198
+ None, help="Custom mount path (default: ~/basic-memory-{tenant-id})"
199
+ ),
200
+ ) -> None:
201
+ """Mount cloud files locally for editing."""
202
+ try:
203
+ mount_cloud_files(profile_name=profile)
204
+ except Exception as e:
205
+ console.print(f"[red]Mount failed: {e}[/red]")
206
+ raise typer.Exit(1)
207
+
208
+
209
+ @cloud_app.command("unmount")
210
+ def unmount() -> None:
211
+ """Unmount cloud files."""
212
+ try:
213
+ unmount_cloud_files()
214
+ except Exception as e:
215
+ console.print(f"[red]Unmount failed: {e}[/red]")
216
+ raise typer.Exit(1)
217
+
218
+
219
+ # Bisync commands
220
+
221
+
222
+ @cloud_app.command("bisync")
223
+ def bisync(
224
+ profile: str = typer.Option(
225
+ "balanced", help=f"Bisync profile: {', '.join(BISYNC_PROFILES.keys())}"
226
+ ),
227
+ dry_run: bool = typer.Option(False, "--dry-run", help="Preview changes without syncing"),
228
+ resync: bool = typer.Option(False, "--resync", help="Force resync to establish new baseline"),
229
+ watch: bool = typer.Option(False, "--watch", help="Run continuous sync in watch mode"),
230
+ interval: int = typer.Option(60, "--interval", help="Sync interval in seconds for watch mode"),
231
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed sync output"),
232
+ ) -> None:
233
+ """Run bidirectional sync between local files and cloud storage.
234
+
235
+ Examples:
236
+ basic-memory cloud bisync # Manual sync with balanced profile
237
+ basic-memory cloud bisync --dry-run # Preview what would be synced
238
+ basic-memory cloud bisync --resync # Establish new baseline
239
+ basic-memory cloud bisync --watch # Continuous sync every 60s
240
+ basic-memory cloud bisync --watch --interval 30 # Continuous sync every 30s
241
+ basic-memory cloud bisync --profile safe # Use safe profile (keep conflicts)
242
+ basic-memory cloud bisync --verbose # Show detailed file sync output
243
+ """
244
+ try:
245
+ if watch:
246
+ run_bisync_watch(profile_name=profile, interval_seconds=interval)
247
+ else:
248
+ run_bisync(profile_name=profile, dry_run=dry_run, resync=resync, verbose=verbose)
249
+ except Exception as e:
250
+ console.print(f"[red]Bisync failed: {e}[/red]")
251
+ raise typer.Exit(1)
252
+
253
+
254
+ @cloud_app.command("bisync-status")
255
+ def bisync_status() -> None:
256
+ """Show current bisync status and configuration.
257
+
258
+ DEPRECATED: Use 'bm cloud status' instead (bisync is now the default).
259
+ """
260
+ console.print(
261
+ "[yellow]Note: 'bisync-status' is deprecated. Use 'bm cloud status' instead.[/yellow]"
262
+ )
263
+ console.print("[dim]Showing bisync status...[/dim]\n")
264
+ show_bisync_status()
265
+
266
+
267
+ @cloud_app.command("check")
268
+ def check(
269
+ one_way: bool = typer.Option(
270
+ False,
271
+ "--one-way",
272
+ help="Only check for missing files on destination (faster)",
273
+ ),
274
+ ) -> None:
275
+ """Check file integrity between local and cloud storage using rclone check.
276
+
277
+ Verifies that files match between your local bisync directory and cloud storage
278
+ without transferring any data. This is useful for validating sync integrity.
279
+
280
+ Examples:
281
+ bm cloud check # Full integrity check
282
+ bm cloud check --one-way # Faster check (missing files only)
283
+ """
284
+ try:
285
+ run_check(one_way=one_way)
286
+ except Exception as e:
287
+ console.print(f"[red]Check failed: {e}[/red]")
288
+ raise typer.Exit(1)
@@ -0,0 +1,295 @@
1
+ """Cloud mount commands for Basic Memory CLI."""
2
+
3
+ import asyncio
4
+ import subprocess
5
+ import time
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from basic_memory.cli.commands.cloud.api_client import CloudAPIError, make_api_request
14
+ from basic_memory.cli.commands.cloud.rclone_config import (
15
+ MOUNT_PROFILES,
16
+ add_tenant_to_rclone_config,
17
+ build_mount_command,
18
+ cleanup_orphaned_rclone_processes,
19
+ get_default_mount_path,
20
+ get_rclone_processes,
21
+ is_path_mounted,
22
+ unmount_path,
23
+ )
24
+ from basic_memory.cli.commands.cloud.rclone_installer import RcloneInstallError, install_rclone
25
+ from basic_memory.config import ConfigManager
26
+
27
+ console = Console()
28
+
29
+
30
+ class MountError(Exception):
31
+ """Exception raised for mount-related errors."""
32
+
33
+ pass
34
+
35
+
36
+ async def get_tenant_info() -> dict:
37
+ """Get current tenant information from cloud API."""
38
+ try:
39
+ config_manager = ConfigManager()
40
+ config = config_manager.config
41
+ host_url = config.cloud_host.rstrip("/")
42
+
43
+ response = await make_api_request(method="GET", url=f"{host_url}/tenant/mount/info")
44
+
45
+ return response.json()
46
+ except Exception as e:
47
+ raise MountError(f"Failed to get tenant info: {e}") from e
48
+
49
+
50
+ async def generate_mount_credentials(tenant_id: str) -> dict:
51
+ """Generate scoped credentials for mounting."""
52
+ try:
53
+ config_manager = ConfigManager()
54
+ config = config_manager.config
55
+ host_url = config.cloud_host.rstrip("/")
56
+
57
+ response = await make_api_request(method="POST", url=f"{host_url}/tenant/mount/credentials")
58
+
59
+ return response.json()
60
+ except Exception as e:
61
+ raise MountError(f"Failed to generate mount credentials: {e}") from e
62
+
63
+
64
+ def setup_cloud_mount() -> None:
65
+ """Set up cloud mount with rclone installation and configuration."""
66
+ console.print("[bold blue]Basic Memory Cloud Setup[/bold blue]")
67
+ console.print("Setting up local file access to your cloud tenant...\n")
68
+
69
+ try:
70
+ # Step 1: Install rclone
71
+ console.print("[blue]Step 1: Installing rclone...[/blue]")
72
+ install_rclone()
73
+
74
+ # Step 2: Get tenant info
75
+ console.print("\n[blue]Step 2: Getting tenant information...[/blue]")
76
+ tenant_info = asyncio.run(get_tenant_info())
77
+
78
+ tenant_id = tenant_info.get("tenant_id")
79
+ bucket_name = tenant_info.get("bucket_name")
80
+
81
+ if not tenant_id or not bucket_name:
82
+ raise MountError("Invalid tenant information received from cloud API")
83
+
84
+ console.print(f"[green]✓ Found tenant: {tenant_id}[/green]")
85
+ console.print(f"[green]✓ Bucket: {bucket_name}[/green]")
86
+
87
+ # Step 3: Generate mount credentials
88
+ console.print("\n[blue]Step 3: Generating mount credentials...[/blue]")
89
+ creds = asyncio.run(generate_mount_credentials(tenant_id))
90
+
91
+ access_key = creds.get("access_key")
92
+ secret_key = creds.get("secret_key")
93
+
94
+ if not access_key or not secret_key:
95
+ raise MountError("Failed to generate mount credentials")
96
+
97
+ console.print("[green]✓ Generated secure credentials[/green]")
98
+
99
+ # Step 4: Configure rclone
100
+ console.print("\n[blue]Step 4: Configuring rclone...[/blue]")
101
+ add_tenant_to_rclone_config(
102
+ tenant_id=tenant_id,
103
+ bucket_name=bucket_name,
104
+ access_key=access_key,
105
+ secret_key=secret_key,
106
+ )
107
+
108
+ # Step 5: Perform initial mount
109
+ console.print("\n[blue]Step 5: Mounting cloud files...[/blue]")
110
+ mount_path = get_default_mount_path()
111
+ MOUNT_PROFILES["balanced"]
112
+
113
+ mount_cloud_files(
114
+ tenant_id=tenant_id,
115
+ bucket_name=bucket_name,
116
+ mount_path=mount_path,
117
+ profile_name="balanced",
118
+ )
119
+
120
+ console.print("\n[bold green]✓ Cloud setup completed successfully![/bold green]")
121
+ console.print("\nYour cloud files are now accessible at:")
122
+ console.print(f" {mount_path}")
123
+ console.print("\nYou can now edit files locally and they will sync to the cloud!")
124
+ console.print("\nUseful commands:")
125
+ console.print(" basic-memory cloud mount-status # Check mount status")
126
+ console.print(" basic-memory cloud unmount # Unmount files")
127
+ console.print(" basic-memory cloud mount --profile fast # Remount with faster sync")
128
+
129
+ except (RcloneInstallError, MountError, CloudAPIError) as e:
130
+ console.print(f"\n[red]Setup failed: {e}[/red]")
131
+ raise typer.Exit(1)
132
+ except Exception as e:
133
+ console.print(f"\n[red]Unexpected error during setup: {e}[/red]")
134
+ raise typer.Exit(1)
135
+
136
+
137
+ def mount_cloud_files(
138
+ tenant_id: Optional[str] = None,
139
+ bucket_name: Optional[str] = None,
140
+ mount_path: Optional[Path] = None,
141
+ profile_name: str = "balanced",
142
+ ) -> None:
143
+ """Mount cloud files with specified profile."""
144
+
145
+ try:
146
+ # Get tenant info if not provided
147
+ if not tenant_id or not bucket_name:
148
+ tenant_info = asyncio.run(get_tenant_info())
149
+ tenant_id = tenant_info.get("tenant_id")
150
+ bucket_name = tenant_info.get("bucket_name")
151
+
152
+ if not tenant_id or not bucket_name:
153
+ raise MountError("Could not determine tenant information")
154
+
155
+ # Set default mount path if not provided
156
+ if not mount_path:
157
+ mount_path = get_default_mount_path()
158
+
159
+ # Get mount profile
160
+ if profile_name not in MOUNT_PROFILES:
161
+ raise MountError(
162
+ f"Unknown profile: {profile_name}. Available: {list(MOUNT_PROFILES.keys())}"
163
+ )
164
+
165
+ profile = MOUNT_PROFILES[profile_name]
166
+
167
+ # Check if already mounted
168
+ if is_path_mounted(mount_path):
169
+ console.print(f"[yellow]Path {mount_path} is already mounted[/yellow]")
170
+ console.print("Use 'basic-memory cloud unmount' first, or mount to a different path")
171
+ return
172
+
173
+ # Create mount directory
174
+ mount_path.mkdir(parents=True, exist_ok=True)
175
+
176
+ # Build and execute mount command
177
+ mount_cmd = build_mount_command(tenant_id, bucket_name, mount_path, profile)
178
+
179
+ console.print(
180
+ f"[blue]Mounting with profile '{profile_name}' ({profile.description})...[/blue]"
181
+ )
182
+ console.print(f"[dim]Command: {' '.join(mount_cmd)}[/dim]")
183
+
184
+ result = subprocess.run(mount_cmd, capture_output=True, text=True)
185
+
186
+ if result.returncode != 0:
187
+ error_msg = result.stderr or "Unknown error"
188
+ raise MountError(f"Mount command failed: {error_msg}")
189
+
190
+ # Wait a moment for mount to establish
191
+ time.sleep(2)
192
+
193
+ # Verify mount
194
+ if is_path_mounted(mount_path):
195
+ console.print(f"[green]✓ Successfully mounted to {mount_path}[/green]")
196
+ console.print(f"[green]✓ Sync profile: {profile.description}[/green]")
197
+ else:
198
+ raise MountError("Mount command succeeded but path is not mounted")
199
+
200
+ except MountError:
201
+ raise
202
+ except Exception as e:
203
+ raise MountError(f"Unexpected error during mount: {e}") from e
204
+
205
+
206
+ def unmount_cloud_files(tenant_id: Optional[str] = None) -> None:
207
+ """Unmount cloud files."""
208
+
209
+ try:
210
+ # Get tenant info if not provided
211
+ if not tenant_id:
212
+ tenant_info = asyncio.run(get_tenant_info())
213
+ tenant_id = tenant_info.get("tenant_id")
214
+
215
+ if not tenant_id:
216
+ raise MountError("Could not determine tenant ID")
217
+
218
+ mount_path = get_default_mount_path()
219
+
220
+ if not is_path_mounted(mount_path):
221
+ console.print(f"[yellow]Path {mount_path} is not mounted[/yellow]")
222
+ return
223
+
224
+ console.print(f"[blue]Unmounting {mount_path}...[/blue]")
225
+
226
+ # Unmount the path
227
+ if unmount_path(mount_path):
228
+ console.print(f"[green]✓ Successfully unmounted {mount_path}[/green]")
229
+
230
+ # Clean up any orphaned rclone processes
231
+ killed_count = cleanup_orphaned_rclone_processes()
232
+ if killed_count > 0:
233
+ console.print(
234
+ f"[green]✓ Cleaned up {killed_count} orphaned rclone process(es)[/green]"
235
+ )
236
+ else:
237
+ console.print(f"[red]✗ Failed to unmount {mount_path}[/red]")
238
+ console.print("You may need to manually unmount or restart your system")
239
+
240
+ except MountError:
241
+ raise
242
+ except Exception as e:
243
+ raise MountError(f"Unexpected error during unmount: {e}") from e
244
+
245
+
246
+ def show_mount_status() -> None:
247
+ """Show current mount status and running processes."""
248
+
249
+ try:
250
+ # Get tenant info
251
+ tenant_info = asyncio.run(get_tenant_info())
252
+ tenant_id = tenant_info.get("tenant_id")
253
+
254
+ if not tenant_id:
255
+ console.print("[red]Could not determine tenant ID[/red]")
256
+ return
257
+
258
+ mount_path = get_default_mount_path()
259
+
260
+ # Create status table
261
+ table = Table(title="Cloud Mount Status", show_header=True, header_style="bold blue")
262
+ table.add_column("Property", style="green", min_width=15)
263
+ table.add_column("Value", style="dim", min_width=30)
264
+
265
+ # Check mount status
266
+ is_mounted = is_path_mounted(mount_path)
267
+ mount_status = "[green]✓ Mounted[/green]" if is_mounted else "[red]✗ Not mounted[/red]"
268
+
269
+ table.add_row("Tenant ID", tenant_id)
270
+ table.add_row("Mount Path", str(mount_path))
271
+ table.add_row("Status", mount_status)
272
+
273
+ # Get rclone processes
274
+ processes = get_rclone_processes()
275
+ if processes:
276
+ table.add_row("rclone Processes", f"{len(processes)} running")
277
+ else:
278
+ table.add_row("rclone Processes", "None")
279
+
280
+ console.print(table)
281
+
282
+ # Show running processes details
283
+ if processes:
284
+ console.print("\n[bold]Running rclone processes:[/bold]")
285
+ for proc in processes:
286
+ console.print(f" PID {proc['pid']}: {proc['command'][:80]}...")
287
+
288
+ # Show mount profiles
289
+ console.print("\n[bold]Available mount profiles:[/bold]")
290
+ for name, profile in MOUNT_PROFILES.items():
291
+ console.print(f" {name}: {profile.description}")
292
+
293
+ except Exception as e:
294
+ console.print(f"[red]Error getting mount status: {e}[/red]")
295
+ raise typer.Exit(1)