basic-memory 0.15.1__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/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/project.py +116 -101
- basic_memory/schemas/cloud.py +7 -3
- {basic_memory-0.15.1.dist-info → basic_memory-0.15.2.dist-info}/METADATA +1 -1
- {basic_memory-0.15.1.dist-info → basic_memory-0.15.2.dist-info}/RECORD +13 -10
- {basic_memory-0.15.1.dist-info → basic_memory-0.15.2.dist-info}/WHEEL +0 -0
- {basic_memory-0.15.1.dist-info → basic_memory-0.15.2.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.15.1.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"
|
|
@@ -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())
|
|
@@ -31,8 +31,6 @@ console = Console()
|
|
|
31
31
|
project_app = typer.Typer(help="Manage multiple Basic Memory projects")
|
|
32
32
|
app.add_typer(project_app, name="project")
|
|
33
33
|
|
|
34
|
-
config = ConfigManager().config
|
|
35
|
-
|
|
36
34
|
|
|
37
35
|
def format_path(path: str) -> str:
|
|
38
36
|
"""Format a path for display, using ~ for home directory."""
|
|
@@ -69,40 +67,34 @@ def list_projects() -> None:
|
|
|
69
67
|
raise typer.Exit(1)
|
|
70
68
|
|
|
71
69
|
|
|
72
|
-
|
|
70
|
+
@project_app.command("add")
|
|
71
|
+
def add_project(
|
|
72
|
+
name: str = typer.Argument(..., help="Name of the project"),
|
|
73
|
+
path: str = typer.Argument(
|
|
74
|
+
None, help="Path to the project directory (required for local mode)"
|
|
75
|
+
),
|
|
76
|
+
set_default: bool = typer.Option(False, "--default", help="Set as default project"),
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Add a new project.
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
) -> None:
|
|
79
|
-
"""Add a new project to Basic Memory Cloud"""
|
|
80
|
+
For cloud mode: only name is required
|
|
81
|
+
For local mode: both name and path are required
|
|
82
|
+
"""
|
|
83
|
+
config = ConfigManager().config
|
|
80
84
|
|
|
85
|
+
if config.cloud_mode_enabled:
|
|
86
|
+
# Cloud mode: path not needed (auto-generated from name)
|
|
81
87
|
async def _add_project():
|
|
82
88
|
async with get_client() as client:
|
|
83
89
|
data = {"name": name, "path": generate_permalink(name), "set_default": set_default}
|
|
84
90
|
response = await call_post(client, "/projects/projects", json=data)
|
|
85
91
|
return ProjectStatusResponse.model_validate(response.json())
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
console.print(
|
|
90
|
-
except Exception as e:
|
|
91
|
-
console.print(f"[red]Error adding project: {str(e)}[/red]")
|
|
92
|
+
else:
|
|
93
|
+
# Local mode: path is required
|
|
94
|
+
if path is None:
|
|
95
|
+
console.print("[red]Error: path argument is required in local mode[/red]")
|
|
92
96
|
raise typer.Exit(1)
|
|
93
97
|
|
|
94
|
-
# Display usage hint
|
|
95
|
-
console.print("\nTo use this project:")
|
|
96
|
-
console.print(f" basic-memory --project={name} <command>")
|
|
97
|
-
else:
|
|
98
|
-
|
|
99
|
-
@project_app.command("add")
|
|
100
|
-
def add_project(
|
|
101
|
-
name: str = typer.Argument(..., help="Name of the project"),
|
|
102
|
-
path: str = typer.Argument(..., help="Path to the project directory"),
|
|
103
|
-
set_default: bool = typer.Option(False, "--default", help="Set as default project"),
|
|
104
|
-
) -> None:
|
|
105
|
-
"""Add a new project."""
|
|
106
98
|
# Resolve to absolute path
|
|
107
99
|
resolved_path = Path(os.path.abspath(os.path.expanduser(path))).as_posix()
|
|
108
100
|
|
|
@@ -112,16 +104,16 @@ else:
|
|
|
112
104
|
response = await call_post(client, "/projects/projects", json=data)
|
|
113
105
|
return ProjectStatusResponse.model_validate(response.json())
|
|
114
106
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
107
|
+
try:
|
|
108
|
+
result = asyncio.run(_add_project())
|
|
109
|
+
console.print(f"[green]{result.message}[/green]")
|
|
110
|
+
except Exception as e:
|
|
111
|
+
console.print(f"[red]Error adding project: {str(e)}[/red]")
|
|
112
|
+
raise typer.Exit(1)
|
|
121
113
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
114
|
+
# Display usage hint
|
|
115
|
+
console.print("\nTo use this project:")
|
|
116
|
+
console.print(f" basic-memory --project={name} <command>")
|
|
125
117
|
|
|
126
118
|
|
|
127
119
|
@project_app.command("remove")
|
|
@@ -147,84 +139,107 @@ def remove_project(
|
|
|
147
139
|
console.print("[yellow]Note: The project files have not been deleted from disk.[/yellow]")
|
|
148
140
|
|
|
149
141
|
|
|
150
|
-
|
|
142
|
+
@project_app.command("default")
|
|
143
|
+
def set_default_project(
|
|
144
|
+
name: str = typer.Argument(..., help="Name of the project to set as CLI default"),
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Set the default project when 'config.default_project_mode' is set.
|
|
151
147
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
) -> None:
|
|
156
|
-
"""Set the default project when 'config.default_project_mode' is set."""
|
|
148
|
+
Note: This command is only available in local mode.
|
|
149
|
+
"""
|
|
150
|
+
config = ConfigManager().config
|
|
157
151
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
response = await call_put(client, f"/projects/{project_permalink}/default")
|
|
162
|
-
return ProjectStatusResponse.model_validate(response.json())
|
|
152
|
+
if config.cloud_mode_enabled:
|
|
153
|
+
console.print("[red]Error: 'default' command is not available in cloud mode[/red]")
|
|
154
|
+
raise typer.Exit(1)
|
|
163
155
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
raise typer.Exit(1)
|
|
156
|
+
async def _set_default():
|
|
157
|
+
async with get_client() as client:
|
|
158
|
+
project_permalink = generate_permalink(name)
|
|
159
|
+
response = await call_put(client, f"/projects/{project_permalink}/default")
|
|
160
|
+
return ProjectStatusResponse.model_validate(response.json())
|
|
170
161
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
"
|
|
162
|
+
try:
|
|
163
|
+
result = asyncio.run(_set_default())
|
|
164
|
+
console.print(f"[green]{result.message}[/green]")
|
|
165
|
+
except Exception as e:
|
|
166
|
+
console.print(f"[red]Error setting default project: {str(e)}[/red]")
|
|
167
|
+
raise typer.Exit(1)
|
|
174
168
|
|
|
175
|
-
async def _sync_config():
|
|
176
|
-
async with get_client() as client:
|
|
177
|
-
response = await call_post(client, "/projects/config/sync")
|
|
178
|
-
return ProjectStatusResponse.model_validate(response.json())
|
|
179
169
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
except Exception as e: # pragma: no cover
|
|
184
|
-
console.print(f"[red]Error synchronizing projects: {str(e)}[/red]")
|
|
185
|
-
raise typer.Exit(1)
|
|
170
|
+
@project_app.command("sync-config")
|
|
171
|
+
def synchronize_projects() -> None:
|
|
172
|
+
"""Synchronize project config between configuration file and database.
|
|
186
173
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
new_path: str = typer.Argument(..., help="New absolute path for the project"),
|
|
191
|
-
) -> None:
|
|
192
|
-
"""Move a project to a new location."""
|
|
193
|
-
# Resolve to absolute path
|
|
194
|
-
resolved_path = Path(os.path.abspath(os.path.expanduser(new_path))).as_posix()
|
|
174
|
+
Note: This command is only available in local mode.
|
|
175
|
+
"""
|
|
176
|
+
config = ConfigManager().config
|
|
195
177
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
project_permalink = generate_permalink(name)
|
|
178
|
+
if config.cloud_mode_enabled:
|
|
179
|
+
console.print("[red]Error: 'sync-config' command is not available in cloud mode[/red]")
|
|
180
|
+
raise typer.Exit(1)
|
|
200
181
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return ProjectStatusResponse.model_validate(response.json())
|
|
182
|
+
async def _sync_config():
|
|
183
|
+
async with get_client() as client:
|
|
184
|
+
response = await call_post(client, "/projects/config/sync")
|
|
185
|
+
return ProjectStatusResponse.model_validate(response.json())
|
|
206
186
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
187
|
+
try:
|
|
188
|
+
result = asyncio.run(_sync_config())
|
|
189
|
+
console.print(f"[green]{result.message}[/green]")
|
|
190
|
+
except Exception as e: # pragma: no cover
|
|
191
|
+
console.print(f"[red]Error synchronizing projects: {str(e)}[/red]")
|
|
192
|
+
raise typer.Exit(1)
|
|
210
193
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
194
|
+
|
|
195
|
+
@project_app.command("move")
|
|
196
|
+
def move_project(
|
|
197
|
+
name: str = typer.Argument(..., help="Name of the project to move"),
|
|
198
|
+
new_path: str = typer.Argument(..., help="New absolute path for the project"),
|
|
199
|
+
) -> None:
|
|
200
|
+
"""Move a project to a new location.
|
|
201
|
+
|
|
202
|
+
Note: This command is only available in local mode.
|
|
203
|
+
"""
|
|
204
|
+
config = ConfigManager().config
|
|
205
|
+
|
|
206
|
+
if config.cloud_mode_enabled:
|
|
207
|
+
console.print("[red]Error: 'move' command is not available in cloud mode[/red]")
|
|
208
|
+
raise typer.Exit(1)
|
|
209
|
+
|
|
210
|
+
# Resolve to absolute path
|
|
211
|
+
resolved_path = Path(os.path.abspath(os.path.expanduser(new_path))).as_posix()
|
|
212
|
+
|
|
213
|
+
async def _move_project():
|
|
214
|
+
async with get_client() as client:
|
|
215
|
+
data = {"path": resolved_path}
|
|
216
|
+
project_permalink = generate_permalink(name)
|
|
217
|
+
|
|
218
|
+
# TODO fix route to use ProjectPathDep
|
|
219
|
+
response = await call_patch(client, f"/{name}/project/{project_permalink}", json=data)
|
|
220
|
+
return ProjectStatusResponse.model_validate(response.json())
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
result = asyncio.run(_move_project())
|
|
224
|
+
console.print(f"[green]{result.message}[/green]")
|
|
225
|
+
|
|
226
|
+
# Show important file movement reminder
|
|
227
|
+
console.print() # Empty line for spacing
|
|
228
|
+
console.print(
|
|
229
|
+
Panel(
|
|
230
|
+
"[bold red]IMPORTANT:[/bold red] Project configuration updated successfully.\n\n"
|
|
231
|
+
"[yellow]You must manually move your project files from the old location to:[/yellow]\n"
|
|
232
|
+
f"[cyan]{resolved_path}[/cyan]\n\n"
|
|
233
|
+
"[dim]Basic Memory has only updated the configuration - your files remain in their original location.[/dim]",
|
|
234
|
+
title="⚠️ Manual File Movement Required",
|
|
235
|
+
border_style="yellow",
|
|
236
|
+
expand=False,
|
|
223
237
|
)
|
|
238
|
+
)
|
|
224
239
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
240
|
+
except Exception as e:
|
|
241
|
+
console.print(f"[red]Error moving project: {str(e)}[/red]")
|
|
242
|
+
raise typer.Exit(1)
|
|
228
243
|
|
|
229
244
|
|
|
230
245
|
@project_app.command("info")
|
basic_memory/schemas/cloud.py
CHANGED
|
@@ -41,6 +41,10 @@ class CloudProjectCreateRequest(BaseModel):
|
|
|
41
41
|
class CloudProjectCreateResponse(BaseModel):
|
|
42
42
|
"""Response from creating a cloud project."""
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
message: str = Field(..., description="Status message about the project creation")
|
|
45
|
+
status: str = Field(..., description="Status of the creation (success or error)")
|
|
46
|
+
default: bool = Field(..., description="True if the project was set as the default")
|
|
47
|
+
old_project: dict | None = Field(None, description="Information about the previous project")
|
|
48
|
+
new_project: dict | None = Field(
|
|
49
|
+
None, description="Information about the newly created project"
|
|
50
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: basic-memory
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.2
|
|
4
4
|
Summary: Local-first knowledge management combining Zettelkasten with knowledge graphs
|
|
5
5
|
Project-URL: Homepage, https://github.com/basicmachines-co/basic-memory
|
|
6
6
|
Project-URL: Repository, https://github.com/basicmachines-co/basic-memory
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
basic_memory/__init__.py,sha256=
|
|
1
|
+
basic_memory/__init__.py,sha256=CsY6W4VzF1LT42OnWaL4Ycc5BYKhSBn8LzNbjgLAuPY,256
|
|
2
2
|
basic_memory/config.py,sha256=bHpnPTTYxPR_dtT1j9mE4y5ucmsow4hn4MplES446bI,17446
|
|
3
3
|
basic_memory/db.py,sha256=Gc-d639GPVzUhNkzkfvOYYuEGeIX9YFqhu6kG_5tR1A,11711
|
|
4
4
|
basic_memory/deps.py,sha256=VpDqUsFHt6TIE4aR4U5jiq6R9sP0bZJGFMYEluuO7ac,13051
|
|
@@ -42,17 +42,20 @@ basic_memory/cli/commands/import_claude_conversations.py,sha256=e8l4OHMr8A9PtKgO
|
|
|
42
42
|
basic_memory/cli/commands/import_claude_projects.py,sha256=YyFXcHWAHJmtR6DNwTtao8nKECoFyo8GripRElqMQ7w,2891
|
|
43
43
|
basic_memory/cli/commands/import_memory_json.py,sha256=3ESHFGdrVQmlh93GYm-AzhKKnDx5cK375ea9EjiKWQw,2867
|
|
44
44
|
basic_memory/cli/commands/mcp.py,sha256=KNyeut5vjXvBjBncZs078YwJTDWb5-CjYpx0bzA4Kjs,3462
|
|
45
|
-
basic_memory/cli/commands/project.py,sha256=
|
|
45
|
+
basic_memory/cli/commands/project.py,sha256=KCNgZrN_0Hm8dsnsVX4wo4UTE988VGAESxQ8p9c9J30,13815
|
|
46
46
|
basic_memory/cli/commands/status.py,sha256=KHrBD6cPFckbmqoadlnFACHao524ZF_v_TzYC_yRPgQ,5872
|
|
47
47
|
basic_memory/cli/commands/sync.py,sha256=OckRY5JWJ2ZCi4-75DuEF-uWMSJX_1FmSRp27AkYtI4,1782
|
|
48
48
|
basic_memory/cli/commands/tool.py,sha256=8bsbYfaYEfAEvLfciQx1fZ0vssgbahMvrbmWzYg9188,11971
|
|
49
|
-
basic_memory/cli/commands/cloud/__init__.py,sha256=
|
|
49
|
+
basic_memory/cli/commands/cloud/__init__.py,sha256=WYDh_GRqKMqhQI21maV2RD1QsDdkgzvRrV9XWa0MJf4,353
|
|
50
50
|
basic_memory/cli/commands/cloud/api_client.py,sha256=e14v_YTkkEuyUuZBzaQTSHpNmT017A3ym5dWMEly5JQ,4263
|
|
51
|
-
basic_memory/cli/commands/cloud/bisync_commands.py,sha256=
|
|
51
|
+
basic_memory/cli/commands/cloud/bisync_commands.py,sha256=8RlClVWpNaXXj170AkF4hAEKSVdZOpol1CG1NT1yRvw,26914
|
|
52
|
+
basic_memory/cli/commands/cloud/cloud_utils.py,sha256=Q3B2UG1el4JSTa89ruFLquZqLJBPfmxQbmXZXQUQ3O0,2977
|
|
52
53
|
basic_memory/cli/commands/cloud/core_commands.py,sha256=OVSVTC9f2_Smp3oNUUCErs3terFZdJIc-GXRvcfK68Q,9694
|
|
53
54
|
basic_memory/cli/commands/cloud/mount_commands.py,sha256=2CZPza3rPiECI5dOLowq3SEzmRQTFdUjopn_W_QQU9Y,10430
|
|
54
55
|
basic_memory/cli/commands/cloud/rclone_config.py,sha256=LpI3_PBKT5qYPG2tV3L9erl4WQQedm97g4x8PC20BP0,8155
|
|
55
56
|
basic_memory/cli/commands/cloud/rclone_installer.py,sha256=x62TjzwDUSwWTy7NVjdwtu9SKO7N5NVls_tfqp2_DDw,7399
|
|
57
|
+
basic_memory/cli/commands/cloud/upload.py,sha256=EPI6aFh3x4XFSE6wKcP-bD2SlXxh0lHYrRVXM7LC5M0,4064
|
|
58
|
+
basic_memory/cli/commands/cloud/upload_command.py,sha256=OsERVwVctgdkFi7ci3C5PWXDwGPqNXOdBtFfElJx6-o,3011
|
|
56
59
|
basic_memory/importers/__init__.py,sha256=BTcBW97P3thcsWa5w9tQsvOu8ynHDgw2-8tPgkCZoh8,795
|
|
57
60
|
basic_memory/importers/base.py,sha256=awwe_U-CfzSINKoM6iro7ru4QqLlsfXzdHztDvebnxM,2531
|
|
58
61
|
basic_memory/importers/chatgpt_importer.py,sha256=3BJZUOVSX0cg9G6WdMTDQTscMoG6eWuf6E-c9Qhi0v4,7687
|
|
@@ -110,7 +113,7 @@ basic_memory/repository/repository.py,sha256=GUKlgBOFvMeFBkmqh80MNyC2YKQdMdPQjlT
|
|
|
110
113
|
basic_memory/repository/search_repository.py,sha256=bs9FXekHY_AYDOzA7L6ZCzf1EtHi1Q3k8HjjCbBXl8E,23989
|
|
111
114
|
basic_memory/schemas/__init__.py,sha256=6bZUVwc-Bvd6yKdNigUslYS3jFYgIQ9eqT-eujtEXY4,1785
|
|
112
115
|
basic_memory/schemas/base.py,sha256=t7F7f40EeQEQVJswMdoQJDd2Uh8LUgLHXVFIP6ugx8U,9551
|
|
113
|
-
basic_memory/schemas/cloud.py,sha256=
|
|
116
|
+
basic_memory/schemas/cloud.py,sha256=4cxS5-Lo0teASdP5q9N6dYlR5TdCpO2_5h2zdB84nu8,1847
|
|
114
117
|
basic_memory/schemas/delete.py,sha256=UAR2JK99WMj3gP-yoGWlHD3eZEkvlTSRf8QoYIE-Wfw,1180
|
|
115
118
|
basic_memory/schemas/directory.py,sha256=F9_LrJqRqb_kO08GDKJzXLb2nhbYG2PdVUo5eDD_Kf4,881
|
|
116
119
|
basic_memory/schemas/importer.py,sha256=rDPfQjyjKyjOe26pwp1UH4eDqGwMKfeNs1Fjv5PxOc0,693
|
|
@@ -139,8 +142,8 @@ basic_memory/sync/sync_service.py,sha256=WrR0iyvWtHmMyMTaViqoE1aRnYVqYIjnuWgFCTw
|
|
|
139
142
|
basic_memory/sync/watch_service.py,sha256=vzVdiJh0eLbqYIkLIJp8hwRIj4z52am6Q-_OubC6mbY,19855
|
|
140
143
|
basic_memory/templates/prompts/continue_conversation.hbs,sha256=trrDHSXA5S0JCbInMoUJL04xvCGRB_ku1RHNQHtl6ZI,3076
|
|
141
144
|
basic_memory/templates/prompts/search.hbs,sha256=H1cCIsHKp4VC1GrH2KeUB8pGe5vXFPqb2VPotypmeCA,3098
|
|
142
|
-
basic_memory-0.15.
|
|
143
|
-
basic_memory-0.15.
|
|
144
|
-
basic_memory-0.15.
|
|
145
|
-
basic_memory-0.15.
|
|
146
|
-
basic_memory-0.15.
|
|
145
|
+
basic_memory-0.15.2.dist-info/METADATA,sha256=89H6M6A_nH2JSouO3PgI1z2Kv0ZbV4kJvTVm2SSOGBQ,15670
|
|
146
|
+
basic_memory-0.15.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
147
|
+
basic_memory-0.15.2.dist-info/entry_points.txt,sha256=wvE2mRF6-Pg4weIYcfQ-86NOLZD4WJg7F7TIsRVFLb8,90
|
|
148
|
+
basic_memory-0.15.2.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
149
|
+
basic_memory-0.15.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|