basic-memory 0.15.0__py3-none-any.whl → 0.15.2__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/api/routers/directory_router.py +23 -2
- basic_memory/api/routers/project_router.py +1 -0
- basic_memory/cli/auth.py +2 -2
- basic_memory/cli/commands/cloud/__init__.py +2 -1
- basic_memory/cli/commands/cloud/bisync_commands.py +4 -57
- basic_memory/cli/commands/cloud/cloud_utils.py +100 -0
- basic_memory/cli/commands/cloud/upload.py +128 -0
- basic_memory/cli/commands/cloud/upload_command.py +93 -0
- basic_memory/cli/commands/command_utils.py +11 -28
- basic_memory/cli/commands/mcp.py +72 -67
- basic_memory/cli/commands/project.py +140 -120
- basic_memory/cli/commands/status.py +6 -15
- basic_memory/config.py +55 -9
- basic_memory/deps.py +7 -5
- basic_memory/ignore_utils.py +7 -7
- basic_memory/mcp/async_client.py +102 -4
- basic_memory/mcp/prompts/continue_conversation.py +16 -15
- basic_memory/mcp/prompts/search.py +12 -11
- basic_memory/mcp/resources/ai_assistant_guide.md +185 -453
- basic_memory/mcp/resources/project_info.py +9 -7
- basic_memory/mcp/tools/build_context.py +40 -39
- basic_memory/mcp/tools/canvas.py +21 -20
- basic_memory/mcp/tools/chatgpt_tools.py +11 -2
- basic_memory/mcp/tools/delete_note.py +22 -21
- basic_memory/mcp/tools/edit_note.py +105 -104
- basic_memory/mcp/tools/list_directory.py +98 -95
- basic_memory/mcp/tools/move_note.py +127 -125
- basic_memory/mcp/tools/project_management.py +101 -98
- basic_memory/mcp/tools/read_content.py +64 -63
- basic_memory/mcp/tools/read_note.py +88 -88
- basic_memory/mcp/tools/recent_activity.py +139 -135
- basic_memory/mcp/tools/search.py +27 -26
- basic_memory/mcp/tools/sync_status.py +133 -128
- basic_memory/mcp/tools/utils.py +0 -15
- basic_memory/mcp/tools/view_note.py +14 -28
- basic_memory/mcp/tools/write_note.py +97 -87
- basic_memory/repository/entity_repository.py +60 -0
- basic_memory/repository/repository.py +16 -3
- basic_memory/repository/search_repository.py +42 -0
- basic_memory/schemas/cloud.py +7 -3
- basic_memory/schemas/project_info.py +1 -1
- basic_memory/services/directory_service.py +124 -3
- basic_memory/services/entity_service.py +31 -9
- basic_memory/services/project_service.py +97 -10
- basic_memory/services/search_service.py +16 -8
- basic_memory/sync/sync_service.py +28 -13
- {basic_memory-0.15.0.dist-info → basic_memory-0.15.2.dist-info}/METADATA +51 -4
- {basic_memory-0.15.0.dist-info → basic_memory-0.15.2.dist-info}/RECORD +52 -50
- basic_memory/mcp/tools/headers.py +0 -44
- {basic_memory-0.15.0.dist-info → basic_memory-0.15.2.dist-info}/WHEEL +0 -0
- {basic_memory-0.15.0.dist-info → basic_memory-0.15.2.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.15.0.dist-info → basic_memory-0.15.2.dist-info}/licenses/LICENSE +0 -0
basic_memory/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
|
|
2
2
|
|
|
3
3
|
# Package version - updated by release automation
|
|
4
|
-
__version__ = "0.15.
|
|
4
|
+
__version__ = "0.15.2"
|
|
5
5
|
|
|
6
6
|
# API version for FastAPI - independent of package version
|
|
7
7
|
__api_version__ = "v0"
|
|
@@ -10,7 +10,7 @@ from basic_memory.schemas.directory import DirectoryNode
|
|
|
10
10
|
router = APIRouter(prefix="/directory", tags=["directory"])
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
@router.get("/tree", response_model=DirectoryNode)
|
|
13
|
+
@router.get("/tree", response_model=DirectoryNode, response_model_exclude_none=True)
|
|
14
14
|
async def get_directory_tree(
|
|
15
15
|
directory_service: DirectoryServiceDep,
|
|
16
16
|
project_id: ProjectIdDep,
|
|
@@ -31,7 +31,28 @@ async def get_directory_tree(
|
|
|
31
31
|
return tree
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
@router.get("/
|
|
34
|
+
@router.get("/structure", response_model=DirectoryNode, response_model_exclude_none=True)
|
|
35
|
+
async def get_directory_structure(
|
|
36
|
+
directory_service: DirectoryServiceDep,
|
|
37
|
+
project_id: ProjectIdDep,
|
|
38
|
+
):
|
|
39
|
+
"""Get folder structure for navigation (no files).
|
|
40
|
+
|
|
41
|
+
Optimized endpoint for folder tree navigation. Returns only directory nodes
|
|
42
|
+
without file metadata. For full tree with files, use /directory/tree.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
directory_service: Service for directory operations
|
|
46
|
+
project_id: ID of the current project
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
DirectoryNode tree containing only folders (type="directory")
|
|
50
|
+
"""
|
|
51
|
+
structure = await directory_service.get_directory_structure()
|
|
52
|
+
return structure
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.get("/list", response_model=List[DirectoryNode], response_model_exclude_none=True)
|
|
35
56
|
async def list_directory(
|
|
36
57
|
directory_service: DirectoryServiceDep,
|
|
37
58
|
project_id: ProjectIdDep,
|
|
@@ -194,6 +194,7 @@ async def add_project(
|
|
|
194
194
|
Response confirming the project was added
|
|
195
195
|
"""
|
|
196
196
|
try: # pragma: no cover
|
|
197
|
+
# The service layer now handles cloud mode validation and path sanitization
|
|
197
198
|
await project_service.add_project(
|
|
198
199
|
project_data.name, project_data.path, set_default=project_data.set_default
|
|
199
200
|
)
|
basic_memory/cli/auth.py
CHANGED
|
@@ -244,7 +244,7 @@ class CLIAuth:
|
|
|
244
244
|
|
|
245
245
|
async def login(self) -> bool:
|
|
246
246
|
"""Perform OAuth Device Authorization login flow."""
|
|
247
|
-
console.print("[blue]Initiating
|
|
247
|
+
console.print("[blue]Initiating authentication...[/blue]")
|
|
248
248
|
|
|
249
249
|
# Step 1: Request device authorization
|
|
250
250
|
device_response = await self.request_device_authorization()
|
|
@@ -265,7 +265,7 @@ class CLIAuth:
|
|
|
265
265
|
# Step 4: Save tokens
|
|
266
266
|
self.save_tokens(tokens)
|
|
267
267
|
|
|
268
|
-
console.print("\n[green]✅ Successfully authenticated with
|
|
268
|
+
console.print("\n[green]✅ Successfully authenticated with Basic Memory Cloud![/green]")
|
|
269
269
|
return True
|
|
270
270
|
|
|
271
271
|
def logout(self) -> None:
|
|
@@ -2,4 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
# Import all commands to register them with typer
|
|
4
4
|
from basic_memory.cli.commands.cloud.core_commands import * # noqa: F401,F403
|
|
5
|
-
from basic_memory.cli.commands.cloud.api_client import get_authenticated_headers # noqa: F401
|
|
5
|
+
from basic_memory.cli.commands.cloud.api_client import get_authenticated_headers, get_cloud_config # noqa: F401
|
|
6
|
+
from basic_memory.cli.commands.cloud.upload_command import * # noqa: F401,F403
|
|
@@ -12,6 +12,10 @@ from rich.console import Console
|
|
|
12
12
|
from rich.table import Table
|
|
13
13
|
|
|
14
14
|
from basic_memory.cli.commands.cloud.api_client import CloudAPIError, make_api_request
|
|
15
|
+
from basic_memory.cli.commands.cloud.cloud_utils import (
|
|
16
|
+
create_cloud_project,
|
|
17
|
+
fetch_cloud_projects,
|
|
18
|
+
)
|
|
15
19
|
from basic_memory.cli.commands.cloud.rclone_config import (
|
|
16
20
|
add_tenant_to_rclone_config,
|
|
17
21
|
)
|
|
@@ -21,11 +25,7 @@ from basic_memory.ignore_utils import get_bmignore_path, create_default_bmignore
|
|
|
21
25
|
from basic_memory.schemas.cloud import (
|
|
22
26
|
TenantMountInfo,
|
|
23
27
|
MountCredentials,
|
|
24
|
-
CloudProjectList,
|
|
25
|
-
CloudProjectCreateRequest,
|
|
26
|
-
CloudProjectCreateResponse,
|
|
27
28
|
)
|
|
28
|
-
from basic_memory.utils import generate_permalink
|
|
29
29
|
|
|
30
30
|
console = Console()
|
|
31
31
|
|
|
@@ -110,24 +110,6 @@ async def generate_mount_credentials(tenant_id: str) -> MountCredentials:
|
|
|
110
110
|
raise BisyncError(f"Failed to generate credentials: {e}") from e
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
async def fetch_cloud_projects() -> CloudProjectList:
|
|
114
|
-
"""Fetch list of projects from cloud API.
|
|
115
|
-
|
|
116
|
-
Returns:
|
|
117
|
-
CloudProjectList with projects from cloud
|
|
118
|
-
"""
|
|
119
|
-
try:
|
|
120
|
-
config_manager = ConfigManager()
|
|
121
|
-
config = config_manager.config
|
|
122
|
-
host_url = config.cloud_host.rstrip("/")
|
|
123
|
-
|
|
124
|
-
response = await make_api_request(method="GET", url=f"{host_url}/proxy/projects/projects")
|
|
125
|
-
|
|
126
|
-
return CloudProjectList.model_validate(response.json())
|
|
127
|
-
except Exception as e:
|
|
128
|
-
raise BisyncError(f"Failed to fetch cloud projects: {e}") from e
|
|
129
|
-
|
|
130
|
-
|
|
131
113
|
def scan_local_directories(sync_dir: Path) -> list[str]:
|
|
132
114
|
"""Scan local sync directory for project folders.
|
|
133
115
|
|
|
@@ -148,41 +130,6 @@ def scan_local_directories(sync_dir: Path) -> list[str]:
|
|
|
148
130
|
return directories
|
|
149
131
|
|
|
150
132
|
|
|
151
|
-
async def create_cloud_project(project_name: str) -> CloudProjectCreateResponse:
|
|
152
|
-
"""Create a new project on cloud.
|
|
153
|
-
|
|
154
|
-
Args:
|
|
155
|
-
project_name: Name of project to create
|
|
156
|
-
|
|
157
|
-
Returns:
|
|
158
|
-
CloudProjectCreateResponse with project details from API
|
|
159
|
-
"""
|
|
160
|
-
try:
|
|
161
|
-
config_manager = ConfigManager()
|
|
162
|
-
config = config_manager.config
|
|
163
|
-
host_url = config.cloud_host.rstrip("/")
|
|
164
|
-
|
|
165
|
-
# Use generate_permalink to ensure consistent naming
|
|
166
|
-
project_path = generate_permalink(project_name)
|
|
167
|
-
|
|
168
|
-
project_data = CloudProjectCreateRequest(
|
|
169
|
-
name=project_name,
|
|
170
|
-
path=project_path,
|
|
171
|
-
set_default=False,
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
response = await make_api_request(
|
|
175
|
-
method="POST",
|
|
176
|
-
url=f"{host_url}/proxy/projects/projects",
|
|
177
|
-
headers={"Content-Type": "application/json"},
|
|
178
|
-
json_data=project_data.model_dump(),
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
return CloudProjectCreateResponse.model_validate(response.json())
|
|
182
|
-
except Exception as e:
|
|
183
|
-
raise BisyncError(f"Failed to create cloud project '{project_name}': {e}") from e
|
|
184
|
-
|
|
185
|
-
|
|
186
133
|
def get_bisync_state_path(tenant_id: str) -> Path:
|
|
187
134
|
"""Get path to bisync state directory."""
|
|
188
135
|
return Path.home() / ".basic-memory" / "bisync-state" / tenant_id
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Shared utilities for cloud operations."""
|
|
2
|
+
|
|
3
|
+
from basic_memory.cli.commands.cloud.api_client import make_api_request
|
|
4
|
+
from basic_memory.config import ConfigManager
|
|
5
|
+
from basic_memory.schemas.cloud import (
|
|
6
|
+
CloudProjectList,
|
|
7
|
+
CloudProjectCreateRequest,
|
|
8
|
+
CloudProjectCreateResponse,
|
|
9
|
+
)
|
|
10
|
+
from basic_memory.utils import generate_permalink
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CloudUtilsError(Exception):
|
|
14
|
+
"""Exception raised for cloud utility errors."""
|
|
15
|
+
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def fetch_cloud_projects() -> CloudProjectList:
|
|
20
|
+
"""Fetch list of projects from cloud API.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
CloudProjectList with projects from cloud
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
config_manager = ConfigManager()
|
|
27
|
+
config = config_manager.config
|
|
28
|
+
host_url = config.cloud_host.rstrip("/")
|
|
29
|
+
|
|
30
|
+
response = await make_api_request(method="GET", url=f"{host_url}/proxy/projects/projects")
|
|
31
|
+
|
|
32
|
+
return CloudProjectList.model_validate(response.json())
|
|
33
|
+
except Exception as e:
|
|
34
|
+
raise CloudUtilsError(f"Failed to fetch cloud projects: {e}") from e
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def create_cloud_project(project_name: str) -> CloudProjectCreateResponse:
|
|
38
|
+
"""Create a new project on cloud.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
project_name: Name of project to create
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
CloudProjectCreateResponse with project details from API
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
config_manager = ConfigManager()
|
|
48
|
+
config = config_manager.config
|
|
49
|
+
host_url = config.cloud_host.rstrip("/")
|
|
50
|
+
|
|
51
|
+
# Use generate_permalink to ensure consistent naming
|
|
52
|
+
project_path = generate_permalink(project_name)
|
|
53
|
+
|
|
54
|
+
project_data = CloudProjectCreateRequest(
|
|
55
|
+
name=project_name,
|
|
56
|
+
path=project_path,
|
|
57
|
+
set_default=False,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
response = await make_api_request(
|
|
61
|
+
method="POST",
|
|
62
|
+
url=f"{host_url}/proxy/projects/projects",
|
|
63
|
+
headers={"Content-Type": "application/json"},
|
|
64
|
+
json_data=project_data.model_dump(),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return CloudProjectCreateResponse.model_validate(response.json())
|
|
68
|
+
except Exception as e:
|
|
69
|
+
raise CloudUtilsError(f"Failed to create cloud project '{project_name}': {e}") from e
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def sync_project(project_name: str) -> None:
|
|
73
|
+
"""Trigger sync for a specific project on cloud.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
project_name: Name of project to sync
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
from basic_memory.cli.commands.command_utils import run_sync
|
|
80
|
+
|
|
81
|
+
await run_sync(project=project_name)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise CloudUtilsError(f"Failed to sync project '{project_name}': {e}") from e
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def project_exists(project_name: str) -> bool:
|
|
87
|
+
"""Check if a project exists on cloud.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
project_name: Name of project to check
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if project exists, False otherwise
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
projects = await fetch_cloud_projects()
|
|
97
|
+
project_names = {p.name for p in projects.projects}
|
|
98
|
+
return project_name in project_names
|
|
99
|
+
except Exception:
|
|
100
|
+
return False
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""WebDAV upload functionality for basic-memory projects."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import aiofiles
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from basic_memory.ignore_utils import load_gitignore_patterns, should_ignore_path
|
|
10
|
+
from basic_memory.mcp.async_client import get_client
|
|
11
|
+
from basic_memory.mcp.tools.utils import call_put
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def upload_path(local_path: Path, project_name: str) -> bool:
|
|
15
|
+
"""
|
|
16
|
+
Upload a file or directory to cloud project via WebDAV.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
local_path: Path to local file or directory
|
|
20
|
+
project_name: Name of cloud project (destination)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
True if upload succeeded, False otherwise
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
# Resolve path
|
|
27
|
+
local_path = local_path.resolve()
|
|
28
|
+
|
|
29
|
+
# Check if path exists
|
|
30
|
+
if not local_path.exists():
|
|
31
|
+
print(f"Error: Path does not exist: {local_path}")
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
# Get files to upload
|
|
35
|
+
if local_path.is_file():
|
|
36
|
+
files_to_upload = [(local_path, local_path.name)]
|
|
37
|
+
else:
|
|
38
|
+
files_to_upload = _get_files_to_upload(local_path)
|
|
39
|
+
|
|
40
|
+
if not files_to_upload:
|
|
41
|
+
print("No files found to upload")
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
print(f"Found {len(files_to_upload)} file(s) to upload")
|
|
45
|
+
|
|
46
|
+
# Upload files using httpx
|
|
47
|
+
total_bytes = 0
|
|
48
|
+
|
|
49
|
+
async with get_client() as client:
|
|
50
|
+
for i, (file_path, relative_path) in enumerate(files_to_upload, 1):
|
|
51
|
+
# Build remote path: /webdav/{project_name}/{relative_path}
|
|
52
|
+
remote_path = f"/webdav/{project_name}/{relative_path}"
|
|
53
|
+
print(f"Uploading {relative_path} ({i}/{len(files_to_upload)})")
|
|
54
|
+
|
|
55
|
+
# Read file content asynchronously
|
|
56
|
+
async with aiofiles.open(file_path, "rb") as f:
|
|
57
|
+
content = await f.read()
|
|
58
|
+
|
|
59
|
+
# Upload via HTTP PUT to WebDAV endpoint
|
|
60
|
+
response = await call_put(client, remote_path, content=content)
|
|
61
|
+
response.raise_for_status()
|
|
62
|
+
|
|
63
|
+
total_bytes += file_path.stat().st_size
|
|
64
|
+
|
|
65
|
+
# Format size based on magnitude
|
|
66
|
+
if total_bytes < 1024:
|
|
67
|
+
size_str = f"{total_bytes} bytes"
|
|
68
|
+
elif total_bytes < 1024 * 1024:
|
|
69
|
+
size_str = f"{total_bytes / 1024:.1f} KB"
|
|
70
|
+
else:
|
|
71
|
+
size_str = f"{total_bytes / (1024 * 1024):.1f} MB"
|
|
72
|
+
|
|
73
|
+
print(f"✓ Upload complete: {len(files_to_upload)} file(s) ({size_str})")
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
except httpx.HTTPStatusError as e:
|
|
77
|
+
print(f"Upload failed: HTTP {e.response.status_code} - {e.response.text}")
|
|
78
|
+
return False
|
|
79
|
+
except Exception as e:
|
|
80
|
+
print(f"Upload failed: {e}")
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _get_files_to_upload(directory: Path) -> list[tuple[Path, str]]:
|
|
85
|
+
"""
|
|
86
|
+
Get list of files to upload from directory.
|
|
87
|
+
|
|
88
|
+
Uses .bmignore and .gitignore patterns for filtering.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
directory: Directory to scan
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
List of (absolute_path, relative_path) tuples
|
|
95
|
+
"""
|
|
96
|
+
files = []
|
|
97
|
+
|
|
98
|
+
# Load ignore patterns from .bmignore and .gitignore
|
|
99
|
+
ignore_patterns = load_gitignore_patterns(directory)
|
|
100
|
+
|
|
101
|
+
# Walk through directory
|
|
102
|
+
for root, dirs, filenames in os.walk(directory):
|
|
103
|
+
root_path = Path(root)
|
|
104
|
+
|
|
105
|
+
# Filter directories based on ignore patterns
|
|
106
|
+
filtered_dirs = []
|
|
107
|
+
for d in dirs:
|
|
108
|
+
dir_path = root_path / d
|
|
109
|
+
if not should_ignore_path(dir_path, directory, ignore_patterns):
|
|
110
|
+
filtered_dirs.append(d)
|
|
111
|
+
dirs[:] = filtered_dirs
|
|
112
|
+
|
|
113
|
+
# Process files
|
|
114
|
+
for filename in filenames:
|
|
115
|
+
file_path = root_path / filename
|
|
116
|
+
|
|
117
|
+
# Check if file should be ignored
|
|
118
|
+
if should_ignore_path(file_path, directory, ignore_patterns):
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
# Calculate relative path for remote
|
|
122
|
+
rel_path = file_path.relative_to(directory)
|
|
123
|
+
# Use forward slashes for WebDAV paths
|
|
124
|
+
remote_path = str(rel_path).replace("\\", "/")
|
|
125
|
+
|
|
126
|
+
files.append((file_path, remote_path))
|
|
127
|
+
|
|
128
|
+
return files
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Upload CLI commands for basic-memory projects."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
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.commands.cloud.cloud_utils import (
|
|
11
|
+
create_cloud_project,
|
|
12
|
+
project_exists,
|
|
13
|
+
sync_project,
|
|
14
|
+
)
|
|
15
|
+
from basic_memory.cli.commands.cloud.upload import upload_path
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@cloud_app.command("upload")
|
|
21
|
+
def upload(
|
|
22
|
+
path: Path = typer.Argument(
|
|
23
|
+
...,
|
|
24
|
+
help="Path to local file or directory to upload",
|
|
25
|
+
exists=True,
|
|
26
|
+
readable=True,
|
|
27
|
+
resolve_path=True,
|
|
28
|
+
),
|
|
29
|
+
project: str = typer.Option(
|
|
30
|
+
...,
|
|
31
|
+
"--project",
|
|
32
|
+
"-p",
|
|
33
|
+
help="Cloud project name (destination)",
|
|
34
|
+
),
|
|
35
|
+
create_project: bool = typer.Option(
|
|
36
|
+
False,
|
|
37
|
+
"--create-project",
|
|
38
|
+
"-c",
|
|
39
|
+
help="Create project if it doesn't exist",
|
|
40
|
+
),
|
|
41
|
+
sync: bool = typer.Option(
|
|
42
|
+
True,
|
|
43
|
+
"--sync/--no-sync",
|
|
44
|
+
help="Sync project after upload (default: true)",
|
|
45
|
+
),
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Upload local files or directories to cloud project via WebDAV.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
bm cloud upload ~/my-notes --project research
|
|
51
|
+
bm cloud upload notes.md --project research --create-project
|
|
52
|
+
bm cloud upload ~/docs --project work --no-sync
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
async def _upload():
|
|
56
|
+
# Check if project exists
|
|
57
|
+
if not await project_exists(project):
|
|
58
|
+
if create_project:
|
|
59
|
+
console.print(f"[blue]Creating cloud project '{project}'...[/blue]")
|
|
60
|
+
try:
|
|
61
|
+
await create_cloud_project(project)
|
|
62
|
+
console.print(f"[green]✓ Created project '{project}'[/green]")
|
|
63
|
+
except Exception as e:
|
|
64
|
+
console.print(f"[red]Failed to create project: {e}[/red]")
|
|
65
|
+
raise typer.Exit(1)
|
|
66
|
+
else:
|
|
67
|
+
console.print(
|
|
68
|
+
f"[red]Project '{project}' does not exist.[/red]\n"
|
|
69
|
+
f"[yellow]Options:[/yellow]\n"
|
|
70
|
+
f" 1. Create it first: bm project add {project}\n"
|
|
71
|
+
f" 2. Use --create-project flag to create automatically"
|
|
72
|
+
)
|
|
73
|
+
raise typer.Exit(1)
|
|
74
|
+
|
|
75
|
+
# Perform upload
|
|
76
|
+
console.print(f"[blue]Uploading {path} to project '{project}'...[/blue]")
|
|
77
|
+
success = await upload_path(path, project)
|
|
78
|
+
if not success:
|
|
79
|
+
console.print("[red]Upload failed[/red]")
|
|
80
|
+
raise typer.Exit(1)
|
|
81
|
+
|
|
82
|
+
console.print(f"[green]✅ Successfully uploaded to '{project}'[/green]")
|
|
83
|
+
|
|
84
|
+
# Sync project if requested
|
|
85
|
+
if sync:
|
|
86
|
+
console.print(f"[blue]Syncing project '{project}'...[/blue]")
|
|
87
|
+
try:
|
|
88
|
+
await sync_project(project)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
console.print(f"[yellow]Warning: Sync failed: {e}[/yellow]")
|
|
91
|
+
console.print("[dim]Files uploaded but may not be indexed yet[/dim]")
|
|
92
|
+
|
|
93
|
+
asyncio.run(_upload())
|
|
@@ -7,8 +7,7 @@ import typer
|
|
|
7
7
|
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
|
|
10
|
-
from basic_memory.
|
|
11
|
-
from basic_memory.mcp.async_client import client
|
|
10
|
+
from basic_memory.mcp.async_client import get_client
|
|
12
11
|
|
|
13
12
|
from basic_memory.mcp.tools.utils import call_post, call_get
|
|
14
13
|
from basic_memory.mcp.project_context import get_active_project
|
|
@@ -21,40 +20,24 @@ async def run_sync(project: Optional[str] = None):
|
|
|
21
20
|
"""Run sync operation via API endpoint."""
|
|
22
21
|
|
|
23
22
|
try:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
auth_headers = await get_authenticated_headers()
|
|
30
|
-
|
|
31
|
-
project_item = await get_active_project(client, project, None, headers=auth_headers)
|
|
32
|
-
response = await call_post(
|
|
33
|
-
client, f"{project_item.project_url}/project/sync", headers=auth_headers
|
|
34
|
-
)
|
|
35
|
-
data = response.json()
|
|
36
|
-
console.print(f"[green]✓ {data['message']}[/green]")
|
|
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]")
|
|
37
28
|
except (ToolError, ValueError) as e:
|
|
38
29
|
console.print(f"[red]✗ Sync failed: {e}[/red]")
|
|
39
30
|
raise typer.Exit(1)
|
|
40
31
|
|
|
41
32
|
|
|
42
33
|
async def get_project_info(project: str):
|
|
43
|
-
"""
|
|
34
|
+
"""Get project information via API endpoint."""
|
|
44
35
|
|
|
45
36
|
try:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if config.cloud_mode_enabled:
|
|
51
|
-
auth_headers = await get_authenticated_headers()
|
|
52
|
-
|
|
53
|
-
project_item = await get_active_project(client, project, None, headers=auth_headers)
|
|
54
|
-
response = await call_get(
|
|
55
|
-
client, f"{project_item.project_url}/project/info", headers=auth_headers
|
|
56
|
-
)
|
|
57
|
-
return ProjectInfoResponse.model_validate(response.json())
|
|
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())
|
|
58
41
|
except (ToolError, ValueError) as e:
|
|
59
42
|
console.print(f"[red]✗ Sync failed: {e}[/red]")
|
|
60
43
|
raise typer.Exit(1)
|