basic-memory 0.14.4__py3-none-any.whl → 0.15.1__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.
- basic_memory/__init__.py +1 -1
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +5 -9
- basic_memory/api/app.py +10 -4
- basic_memory/api/routers/directory_router.py +23 -2
- basic_memory/api/routers/knowledge_router.py +25 -8
- basic_memory/api/routers/project_router.py +100 -4
- basic_memory/cli/app.py +9 -28
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/cloud/__init__.py +5 -0
- basic_memory/cli/commands/cloud/api_client.py +112 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +818 -0
- basic_memory/cli/commands/cloud/core_commands.py +288 -0
- basic_memory/cli/commands/cloud/mount_commands.py +295 -0
- basic_memory/cli/commands/cloud/rclone_config.py +288 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +198 -0
- basic_memory/cli/commands/command_utils.py +43 -0
- basic_memory/cli/commands/import_memory_json.py +0 -4
- basic_memory/cli/commands/mcp.py +77 -60
- basic_memory/cli/commands/project.py +154 -152
- basic_memory/cli/commands/status.py +25 -22
- basic_memory/cli/commands/sync.py +45 -228
- basic_memory/cli/commands/tool.py +87 -16
- basic_memory/cli/main.py +1 -0
- basic_memory/config.py +131 -21
- basic_memory/db.py +104 -3
- basic_memory/deps.py +27 -8
- basic_memory/file_utils.py +37 -13
- basic_memory/ignore_utils.py +295 -0
- basic_memory/markdown/plugins.py +9 -7
- basic_memory/mcp/async_client.py +124 -14
- basic_memory/mcp/project_context.py +141 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +49 -4
- basic_memory/mcp/prompts/continue_conversation.py +17 -16
- basic_memory/mcp/prompts/recent_activity.py +116 -32
- basic_memory/mcp/prompts/search.py +13 -12
- basic_memory/mcp/prompts/utils.py +11 -4
- basic_memory/mcp/resources/ai_assistant_guide.md +211 -341
- basic_memory/mcp/resources/project_info.py +27 -11
- basic_memory/mcp/server.py +0 -37
- basic_memory/mcp/tools/__init__.py +5 -6
- basic_memory/mcp/tools/build_context.py +67 -56
- basic_memory/mcp/tools/canvas.py +38 -26
- basic_memory/mcp/tools/chatgpt_tools.py +187 -0
- basic_memory/mcp/tools/delete_note.py +81 -47
- basic_memory/mcp/tools/edit_note.py +155 -138
- basic_memory/mcp/tools/list_directory.py +112 -99
- basic_memory/mcp/tools/move_note.py +181 -101
- basic_memory/mcp/tools/project_management.py +113 -277
- basic_memory/mcp/tools/read_content.py +91 -74
- basic_memory/mcp/tools/read_note.py +152 -115
- basic_memory/mcp/tools/recent_activity.py +471 -68
- basic_memory/mcp/tools/search.py +105 -92
- basic_memory/mcp/tools/sync_status.py +136 -130
- basic_memory/mcp/tools/utils.py +4 -0
- basic_memory/mcp/tools/view_note.py +44 -33
- basic_memory/mcp/tools/write_note.py +151 -90
- basic_memory/models/knowledge.py +12 -6
- basic_memory/models/project.py +6 -2
- basic_memory/repository/entity_repository.py +89 -82
- basic_memory/repository/relation_repository.py +13 -0
- basic_memory/repository/repository.py +18 -5
- basic_memory/repository/search_repository.py +46 -2
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/base.py +39 -11
- basic_memory/schemas/cloud.py +46 -0
- basic_memory/schemas/memory.py +90 -21
- basic_memory/schemas/project_info.py +9 -10
- basic_memory/schemas/sync_report.py +48 -0
- basic_memory/services/context_service.py +25 -11
- basic_memory/services/directory_service.py +124 -3
- basic_memory/services/entity_service.py +100 -48
- basic_memory/services/initialization.py +30 -11
- basic_memory/services/project_service.py +101 -24
- basic_memory/services/search_service.py +16 -8
- basic_memory/sync/sync_service.py +173 -34
- basic_memory/sync/watch_service.py +101 -40
- basic_memory/utils.py +14 -4
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/METADATA +57 -9
- basic_memory-0.15.1.dist-info/RECORD +146 -0
- basic_memory/mcp/project_session.py +0 -120
- basic_memory-0.14.4.dist-info/RECORD +0 -133
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/WHEEL +0 -0
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""rclone configuration management for Basic Memory Cloud."""
|
|
2
|
+
|
|
3
|
+
import configparser
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RcloneConfigError(Exception):
|
|
16
|
+
"""Exception raised for rclone configuration errors."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RcloneMountProfile:
|
|
22
|
+
"""Mount profile with optimized settings."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
name: str,
|
|
27
|
+
cache_time: str,
|
|
28
|
+
poll_interval: str,
|
|
29
|
+
attr_timeout: str,
|
|
30
|
+
write_back: str,
|
|
31
|
+
description: str,
|
|
32
|
+
extra_args: Optional[List[str]] = None,
|
|
33
|
+
):
|
|
34
|
+
self.name = name
|
|
35
|
+
self.cache_time = cache_time
|
|
36
|
+
self.poll_interval = poll_interval
|
|
37
|
+
self.attr_timeout = attr_timeout
|
|
38
|
+
self.write_back = write_back
|
|
39
|
+
self.description = description
|
|
40
|
+
self.extra_args = extra_args or []
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Mount profiles based on SPEC-7 Phase 4 testing
|
|
44
|
+
MOUNT_PROFILES = {
|
|
45
|
+
"fast": RcloneMountProfile(
|
|
46
|
+
name="fast",
|
|
47
|
+
cache_time="5s",
|
|
48
|
+
poll_interval="3s",
|
|
49
|
+
attr_timeout="3s",
|
|
50
|
+
write_back="1s",
|
|
51
|
+
description="Ultra-fast development (5s sync, higher bandwidth)",
|
|
52
|
+
),
|
|
53
|
+
"balanced": RcloneMountProfile(
|
|
54
|
+
name="balanced",
|
|
55
|
+
cache_time="10s",
|
|
56
|
+
poll_interval="5s",
|
|
57
|
+
attr_timeout="5s",
|
|
58
|
+
write_back="2s",
|
|
59
|
+
description="Fast development (10-15s sync, recommended)",
|
|
60
|
+
),
|
|
61
|
+
"safe": RcloneMountProfile(
|
|
62
|
+
name="safe",
|
|
63
|
+
cache_time="15s",
|
|
64
|
+
poll_interval="10s",
|
|
65
|
+
attr_timeout="10s",
|
|
66
|
+
write_back="5s",
|
|
67
|
+
description="Conflict-aware mount with backup",
|
|
68
|
+
extra_args=[
|
|
69
|
+
"--conflict-suffix",
|
|
70
|
+
".conflict-{DateTimeExt}",
|
|
71
|
+
"--backup-dir",
|
|
72
|
+
"~/.basic-memory/conflicts",
|
|
73
|
+
"--track-renames",
|
|
74
|
+
],
|
|
75
|
+
),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_rclone_config_path() -> Path:
|
|
80
|
+
"""Get the path to rclone configuration file."""
|
|
81
|
+
config_dir = Path.home() / ".config" / "rclone"
|
|
82
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
return config_dir / "rclone.conf"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def backup_rclone_config() -> Optional[Path]:
|
|
87
|
+
"""Create a backup of existing rclone config."""
|
|
88
|
+
config_path = get_rclone_config_path()
|
|
89
|
+
if not config_path.exists():
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
backup_path = config_path.with_suffix(f".conf.backup-{os.getpid()}")
|
|
93
|
+
shutil.copy2(config_path, backup_path)
|
|
94
|
+
console.print(f"[dim]Created backup: {backup_path}[/dim]")
|
|
95
|
+
return backup_path
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def load_rclone_config() -> configparser.ConfigParser:
|
|
99
|
+
"""Load existing rclone configuration."""
|
|
100
|
+
config = configparser.ConfigParser()
|
|
101
|
+
config_path = get_rclone_config_path()
|
|
102
|
+
|
|
103
|
+
if config_path.exists():
|
|
104
|
+
config.read(config_path)
|
|
105
|
+
|
|
106
|
+
return config
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def save_rclone_config(config: configparser.ConfigParser) -> None:
|
|
110
|
+
"""Save rclone configuration to file."""
|
|
111
|
+
config_path = get_rclone_config_path()
|
|
112
|
+
|
|
113
|
+
with open(config_path, "w") as f:
|
|
114
|
+
config.write(f)
|
|
115
|
+
|
|
116
|
+
console.print(f"[dim]Updated rclone config: {config_path}[/dim]")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def add_tenant_to_rclone_config(
|
|
120
|
+
tenant_id: str,
|
|
121
|
+
bucket_name: str,
|
|
122
|
+
access_key: str,
|
|
123
|
+
secret_key: str,
|
|
124
|
+
endpoint: str = "https://fly.storage.tigris.dev",
|
|
125
|
+
region: str = "auto",
|
|
126
|
+
) -> str:
|
|
127
|
+
"""Add tenant configuration to rclone config file."""
|
|
128
|
+
|
|
129
|
+
# Backup existing config
|
|
130
|
+
backup_rclone_config()
|
|
131
|
+
|
|
132
|
+
# Load existing config
|
|
133
|
+
config = load_rclone_config()
|
|
134
|
+
|
|
135
|
+
# Create section name
|
|
136
|
+
section_name = f"basic-memory-{tenant_id}"
|
|
137
|
+
|
|
138
|
+
# Add/update the tenant section
|
|
139
|
+
if not config.has_section(section_name):
|
|
140
|
+
config.add_section(section_name)
|
|
141
|
+
|
|
142
|
+
config.set(section_name, "type", "s3")
|
|
143
|
+
config.set(section_name, "provider", "Other")
|
|
144
|
+
config.set(section_name, "access_key_id", access_key)
|
|
145
|
+
config.set(section_name, "secret_access_key", secret_key)
|
|
146
|
+
config.set(section_name, "endpoint", endpoint)
|
|
147
|
+
config.set(section_name, "region", region)
|
|
148
|
+
|
|
149
|
+
# Save updated config
|
|
150
|
+
save_rclone_config(config)
|
|
151
|
+
|
|
152
|
+
console.print(f"[green]✓ Added tenant {tenant_id} to rclone config[/green]")
|
|
153
|
+
return section_name
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def remove_tenant_from_rclone_config(tenant_id: str) -> bool:
|
|
157
|
+
"""Remove tenant configuration from rclone config."""
|
|
158
|
+
config = load_rclone_config()
|
|
159
|
+
section_name = f"basic-memory-{tenant_id}"
|
|
160
|
+
|
|
161
|
+
if config.has_section(section_name):
|
|
162
|
+
backup_rclone_config()
|
|
163
|
+
config.remove_section(section_name)
|
|
164
|
+
save_rclone_config(config)
|
|
165
|
+
console.print(f"[green]✓ Removed tenant {tenant_id} from rclone config[/green]")
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_default_mount_path() -> Path:
|
|
172
|
+
"""Get default mount path (fixed location per SPEC-9).
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Fixed mount path: ~/basic-memory-cloud/
|
|
176
|
+
"""
|
|
177
|
+
return Path.home() / "basic-memory-cloud"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def build_mount_command(
|
|
181
|
+
tenant_id: str, bucket_name: str, mount_path: Path, profile: RcloneMountProfile
|
|
182
|
+
) -> List[str]:
|
|
183
|
+
"""Build rclone mount command with optimized settings."""
|
|
184
|
+
|
|
185
|
+
rclone_remote = f"basic-memory-{tenant_id}:{bucket_name}"
|
|
186
|
+
|
|
187
|
+
cmd = [
|
|
188
|
+
"rclone",
|
|
189
|
+
"nfsmount",
|
|
190
|
+
rclone_remote,
|
|
191
|
+
str(mount_path),
|
|
192
|
+
"--vfs-cache-mode",
|
|
193
|
+
"writes",
|
|
194
|
+
"--dir-cache-time",
|
|
195
|
+
profile.cache_time,
|
|
196
|
+
"--vfs-cache-poll-interval",
|
|
197
|
+
profile.poll_interval,
|
|
198
|
+
"--attr-timeout",
|
|
199
|
+
profile.attr_timeout,
|
|
200
|
+
"--vfs-write-back",
|
|
201
|
+
profile.write_back,
|
|
202
|
+
"--daemon",
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
# Add profile-specific extra arguments
|
|
206
|
+
cmd.extend(profile.extra_args)
|
|
207
|
+
|
|
208
|
+
return cmd
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def is_path_mounted(mount_path: Path) -> bool:
|
|
212
|
+
"""Check if a path is currently mounted."""
|
|
213
|
+
if not mount_path.exists():
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
# Check if mount point is actually mounted by looking for mount table entry
|
|
218
|
+
result = subprocess.run(["mount"], capture_output=True, text=True, check=False)
|
|
219
|
+
|
|
220
|
+
if result.returncode == 0:
|
|
221
|
+
# Look for our mount path in mount output
|
|
222
|
+
mount_str = str(mount_path.resolve())
|
|
223
|
+
return mount_str in result.stdout
|
|
224
|
+
|
|
225
|
+
return False
|
|
226
|
+
except Exception:
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def get_rclone_processes() -> List[Dict[str, str]]:
|
|
231
|
+
"""Get list of running rclone processes."""
|
|
232
|
+
try:
|
|
233
|
+
# Use ps to find rclone processes
|
|
234
|
+
result = subprocess.run(
|
|
235
|
+
["ps", "-eo", "pid,args"], capture_output=True, text=True, check=False
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
processes = []
|
|
239
|
+
if result.returncode == 0:
|
|
240
|
+
for line in result.stdout.split("\n"):
|
|
241
|
+
if "rclone" in line and "basic-memory" in line:
|
|
242
|
+
parts = line.strip().split(None, 1)
|
|
243
|
+
if len(parts) >= 2:
|
|
244
|
+
processes.append({"pid": parts[0], "command": parts[1]})
|
|
245
|
+
|
|
246
|
+
return processes
|
|
247
|
+
except Exception:
|
|
248
|
+
return []
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def kill_rclone_process(pid: str) -> bool:
|
|
252
|
+
"""Kill a specific rclone process."""
|
|
253
|
+
try:
|
|
254
|
+
subprocess.run(["kill", pid], check=True)
|
|
255
|
+
console.print(f"[green]✓ Killed rclone process {pid}[/green]")
|
|
256
|
+
return True
|
|
257
|
+
except subprocess.CalledProcessError:
|
|
258
|
+
console.print(f"[red]✗ Failed to kill rclone process {pid}[/red]")
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def unmount_path(mount_path: Path) -> bool:
|
|
263
|
+
"""Unmount a mounted path."""
|
|
264
|
+
if not is_path_mounted(mount_path):
|
|
265
|
+
return True
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
subprocess.run(["umount", str(mount_path)], check=True)
|
|
269
|
+
console.print(f"[green]✓ Unmounted {mount_path}[/green]")
|
|
270
|
+
return True
|
|
271
|
+
except subprocess.CalledProcessError as e:
|
|
272
|
+
console.print(f"[red]✗ Failed to unmount {mount_path}: {e}[/red]")
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def cleanup_orphaned_rclone_processes() -> int:
|
|
277
|
+
"""Clean up orphaned rclone processes for basic-memory."""
|
|
278
|
+
processes = get_rclone_processes()
|
|
279
|
+
killed_count = 0
|
|
280
|
+
|
|
281
|
+
for proc in processes:
|
|
282
|
+
console.print(
|
|
283
|
+
f"[yellow]Found rclone process: {proc['pid']} - {proc['command'][:80]}...[/yellow]"
|
|
284
|
+
)
|
|
285
|
+
if kill_rclone_process(proc["pid"]):
|
|
286
|
+
killed_count += 1
|
|
287
|
+
|
|
288
|
+
return killed_count
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Cross-platform rclone installation utilities."""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RcloneInstallError(Exception):
|
|
14
|
+
"""Exception raised for rclone installation errors."""
|
|
15
|
+
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_rclone_installed() -> bool:
|
|
20
|
+
"""Check if rclone is already installed and available in PATH."""
|
|
21
|
+
return shutil.which("rclone") is not None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_platform() -> str:
|
|
25
|
+
"""Get the current platform identifier."""
|
|
26
|
+
system = platform.system().lower()
|
|
27
|
+
if system == "darwin":
|
|
28
|
+
return "macos"
|
|
29
|
+
elif system == "linux":
|
|
30
|
+
return "linux"
|
|
31
|
+
elif system == "windows":
|
|
32
|
+
return "windows"
|
|
33
|
+
else:
|
|
34
|
+
raise RcloneInstallError(f"Unsupported platform: {system}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_command(command: list[str], check: bool = True) -> subprocess.CompletedProcess:
|
|
38
|
+
"""Run a command with proper error handling."""
|
|
39
|
+
try:
|
|
40
|
+
console.print(f"[dim]Running: {' '.join(command)}[/dim]")
|
|
41
|
+
result = subprocess.run(command, capture_output=True, text=True, check=check)
|
|
42
|
+
if result.stdout:
|
|
43
|
+
console.print(f"[dim]Output: {result.stdout.strip()}[/dim]")
|
|
44
|
+
return result
|
|
45
|
+
except subprocess.CalledProcessError as e:
|
|
46
|
+
console.print(f"[red]Command failed: {e}[/red]")
|
|
47
|
+
if e.stderr:
|
|
48
|
+
console.print(f"[red]Error output: {e.stderr}[/red]")
|
|
49
|
+
raise RcloneInstallError(f"Command failed: {e}") from e
|
|
50
|
+
except FileNotFoundError as e:
|
|
51
|
+
raise RcloneInstallError(f"Command not found: {' '.join(command)}") from e
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def install_rclone_macos() -> None:
|
|
55
|
+
"""Install rclone on macOS using Homebrew or official script."""
|
|
56
|
+
# Try Homebrew first
|
|
57
|
+
if shutil.which("brew"):
|
|
58
|
+
try:
|
|
59
|
+
console.print("[blue]Installing rclone via Homebrew...[/blue]")
|
|
60
|
+
run_command(["brew", "install", "rclone"])
|
|
61
|
+
console.print("[green]✓ rclone installed via Homebrew[/green]")
|
|
62
|
+
return
|
|
63
|
+
except RcloneInstallError:
|
|
64
|
+
console.print(
|
|
65
|
+
"[yellow]Homebrew installation failed, trying official script...[/yellow]"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Fallback to official script
|
|
69
|
+
console.print("[blue]Installing rclone via official script...[/blue]")
|
|
70
|
+
try:
|
|
71
|
+
run_command(["sh", "-c", "curl https://rclone.org/install.sh | sudo bash"])
|
|
72
|
+
console.print("[green]✓ rclone installed via official script[/green]")
|
|
73
|
+
except RcloneInstallError:
|
|
74
|
+
raise RcloneInstallError(
|
|
75
|
+
"Failed to install rclone. Please install manually: brew install rclone"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def install_rclone_linux() -> None:
|
|
80
|
+
"""Install rclone on Linux using package managers or official script."""
|
|
81
|
+
# Try snap first (most universal)
|
|
82
|
+
if shutil.which("snap"):
|
|
83
|
+
try:
|
|
84
|
+
console.print("[blue]Installing rclone via snap...[/blue]")
|
|
85
|
+
run_command(["sudo", "snap", "install", "rclone"])
|
|
86
|
+
console.print("[green]✓ rclone installed via snap[/green]")
|
|
87
|
+
return
|
|
88
|
+
except RcloneInstallError:
|
|
89
|
+
console.print("[yellow]Snap installation failed, trying apt...[/yellow]")
|
|
90
|
+
|
|
91
|
+
# Try apt (Debian/Ubuntu)
|
|
92
|
+
if shutil.which("apt"):
|
|
93
|
+
try:
|
|
94
|
+
console.print("[blue]Installing rclone via apt...[/blue]")
|
|
95
|
+
run_command(["sudo", "apt", "update"])
|
|
96
|
+
run_command(["sudo", "apt", "install", "-y", "rclone"])
|
|
97
|
+
console.print("[green]✓ rclone installed via apt[/green]")
|
|
98
|
+
return
|
|
99
|
+
except RcloneInstallError:
|
|
100
|
+
console.print("[yellow]apt installation failed, trying official script...[/yellow]")
|
|
101
|
+
|
|
102
|
+
# Fallback to official script
|
|
103
|
+
console.print("[blue]Installing rclone via official script...[/blue]")
|
|
104
|
+
try:
|
|
105
|
+
run_command(["sh", "-c", "curl https://rclone.org/install.sh | sudo bash"])
|
|
106
|
+
console.print("[green]✓ rclone installed via official script[/green]")
|
|
107
|
+
except RcloneInstallError:
|
|
108
|
+
raise RcloneInstallError(
|
|
109
|
+
"Failed to install rclone. Please install manually: sudo snap install rclone"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def install_rclone_windows() -> None:
|
|
114
|
+
"""Install rclone on Windows using package managers."""
|
|
115
|
+
# Try winget first (built into Windows 10+)
|
|
116
|
+
if shutil.which("winget"):
|
|
117
|
+
try:
|
|
118
|
+
console.print("[blue]Installing rclone via winget...[/blue]")
|
|
119
|
+
run_command(["winget", "install", "Rclone.Rclone"])
|
|
120
|
+
console.print("[green]✓ rclone installed via winget[/green]")
|
|
121
|
+
return
|
|
122
|
+
except RcloneInstallError:
|
|
123
|
+
console.print("[yellow]winget installation failed, trying chocolatey...[/yellow]")
|
|
124
|
+
|
|
125
|
+
# Try chocolatey
|
|
126
|
+
if shutil.which("choco"):
|
|
127
|
+
try:
|
|
128
|
+
console.print("[blue]Installing rclone via chocolatey...[/blue]")
|
|
129
|
+
run_command(["choco", "install", "rclone", "-y"])
|
|
130
|
+
console.print("[green]✓ rclone installed via chocolatey[/green]")
|
|
131
|
+
return
|
|
132
|
+
except RcloneInstallError:
|
|
133
|
+
console.print("[yellow]chocolatey installation failed, trying scoop...[/yellow]")
|
|
134
|
+
|
|
135
|
+
# Try scoop
|
|
136
|
+
if shutil.which("scoop"):
|
|
137
|
+
try:
|
|
138
|
+
console.print("[blue]Installing rclone via scoop...[/blue]")
|
|
139
|
+
run_command(["scoop", "install", "rclone"])
|
|
140
|
+
console.print("[green]✓ rclone installed via scoop[/green]")
|
|
141
|
+
return
|
|
142
|
+
except RcloneInstallError:
|
|
143
|
+
console.print("[yellow]scoop installation failed[/yellow]")
|
|
144
|
+
|
|
145
|
+
# No package manager available
|
|
146
|
+
raise RcloneInstallError(
|
|
147
|
+
"Could not install rclone automatically. Please install a package manager "
|
|
148
|
+
"(winget, chocolatey, or scoop) or install rclone manually from https://rclone.org/downloads/"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def install_rclone(platform_override: Optional[str] = None) -> None:
|
|
153
|
+
"""Install rclone for the current platform."""
|
|
154
|
+
if is_rclone_installed():
|
|
155
|
+
console.print("[green]rclone is already installed[/green]")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
platform_name = platform_override or get_platform()
|
|
159
|
+
console.print(f"[blue]Installing rclone for {platform_name}...[/blue]")
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
if platform_name == "macos":
|
|
163
|
+
install_rclone_macos()
|
|
164
|
+
elif platform_name == "linux":
|
|
165
|
+
install_rclone_linux()
|
|
166
|
+
elif platform_name == "windows":
|
|
167
|
+
install_rclone_windows()
|
|
168
|
+
else:
|
|
169
|
+
raise RcloneInstallError(f"Unsupported platform: {platform_name}")
|
|
170
|
+
|
|
171
|
+
# Verify installation
|
|
172
|
+
if not is_rclone_installed():
|
|
173
|
+
raise RcloneInstallError("rclone installation completed but command not found in PATH")
|
|
174
|
+
|
|
175
|
+
console.print("[green]✓ rclone installation completed successfully[/green]")
|
|
176
|
+
|
|
177
|
+
except RcloneInstallError:
|
|
178
|
+
raise
|
|
179
|
+
except Exception as e:
|
|
180
|
+
raise RcloneInstallError(f"Unexpected error during installation: {e}") from e
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def get_rclone_version() -> Optional[str]:
|
|
184
|
+
"""Get the installed rclone version."""
|
|
185
|
+
if not is_rclone_installed():
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
result = run_command(["rclone", "version"], check=False)
|
|
190
|
+
if result.returncode == 0:
|
|
191
|
+
# Parse version from output (format: "rclone v1.64.0")
|
|
192
|
+
lines = result.stdout.strip().split("\n")
|
|
193
|
+
for line in lines:
|
|
194
|
+
if line.startswith("rclone v"):
|
|
195
|
+
return line.split()[1]
|
|
196
|
+
return "unknown"
|
|
197
|
+
except Exception:
|
|
198
|
+
return "unknown"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""utility functions for commands"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp.exceptions import ToolError
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from basic_memory.mcp.async_client import get_client
|
|
11
|
+
|
|
12
|
+
from basic_memory.mcp.tools.utils import call_post, call_get
|
|
13
|
+
from basic_memory.mcp.project_context import get_active_project
|
|
14
|
+
from basic_memory.schemas import ProjectInfoResponse
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def run_sync(project: Optional[str] = None):
|
|
20
|
+
"""Run sync operation via API endpoint."""
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
async with get_client() as client:
|
|
24
|
+
project_item = await get_active_project(client, project, None)
|
|
25
|
+
response = await call_post(client, f"{project_item.project_url}/project/sync")
|
|
26
|
+
data = response.json()
|
|
27
|
+
console.print(f"[green]✓ {data['message']}[/green]")
|
|
28
|
+
except (ToolError, ValueError) as e:
|
|
29
|
+
console.print(f"[red]✗ Sync failed: {e}[/red]")
|
|
30
|
+
raise typer.Exit(1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def get_project_info(project: str):
|
|
34
|
+
"""Get project information via API endpoint."""
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
async with get_client() as client:
|
|
38
|
+
project_item = await get_active_project(client, project, None)
|
|
39
|
+
response = await call_get(client, f"{project_item.project_url}/project/info")
|
|
40
|
+
return ProjectInfoResponse.model_validate(response.json())
|
|
41
|
+
except (ToolError, ValueError) as e:
|
|
42
|
+
console.print(f"[red]✗ Sync failed: {e}[/red]")
|
|
43
|
+
raise typer.Exit(1)
|
|
@@ -39,8 +39,6 @@ def memory_json(
|
|
|
39
39
|
1. Read entities and relations from the JSON file
|
|
40
40
|
2. Create markdown files for each entity
|
|
41
41
|
3. Include outgoing relations in each entity's markdown
|
|
42
|
-
|
|
43
|
-
After importing, run 'basic-memory sync' to index the new files.
|
|
44
42
|
"""
|
|
45
43
|
|
|
46
44
|
if not json_path.exists():
|
|
@@ -82,8 +80,6 @@ def memory_json(
|
|
|
82
80
|
)
|
|
83
81
|
)
|
|
84
82
|
|
|
85
|
-
console.print("\nRun 'basic-memory sync' to index the new files.")
|
|
86
|
-
|
|
87
83
|
except Exception as e:
|
|
88
84
|
logger.error("Import failed")
|
|
89
85
|
typer.echo(f"Error during import: {e}", err=True)
|