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 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.1"
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
- if config.cloud_mode_enabled:
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
- @project_app.command("add")
75
- def add_project_cloud(
76
- name: str = typer.Argument(..., help="Name of the project"),
77
- set_default: bool = typer.Option(False, "--default", help="Set as default project"),
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
- try:
88
- result = asyncio.run(_add_project())
89
- console.print(f"[green]{result.message}[/green]")
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
- try:
116
- result = asyncio.run(_add_project())
117
- console.print(f"[green]{result.message}[/green]")
118
- except Exception as e:
119
- console.print(f"[red]Error adding project: {str(e)}[/red]")
120
- raise typer.Exit(1)
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
- # Display usage hint
123
- console.print("\nTo use this project:")
124
- console.print(f" basic-memory --project={name} <command>")
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
- if not config.cloud_mode_enabled:
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
- @project_app.command("default")
153
- def set_default_project(
154
- name: str = typer.Argument(..., help="Name of the project to set as CLI default"),
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
- async def _set_default():
159
- async with get_client() as client:
160
- project_permalink = generate_permalink(name)
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
- try:
165
- result = asyncio.run(_set_default())
166
- console.print(f"[green]{result.message}[/green]")
167
- except Exception as e:
168
- console.print(f"[red]Error setting default project: {str(e)}[/red]")
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
- @project_app.command("sync-config")
172
- def synchronize_projects() -> None:
173
- """Synchronize project config between configuration file and database."""
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
- try:
181
- result = asyncio.run(_sync_config())
182
- console.print(f"[green]{result.message}[/green]")
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
- @project_app.command("move")
188
- def move_project(
189
- name: str = typer.Argument(..., help="Name of the project to move"),
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
- async def _move_project():
197
- async with get_client() as client:
198
- data = {"path": resolved_path}
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
- # TODO fix route to use ProjectPathDep
202
- response = await call_patch(
203
- client, f"/{name}/project/{project_permalink}", json=data
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
- try:
208
- result = asyncio.run(_move_project())
209
- console.print(f"[green]{result.message}[/green]")
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
- # Show important file movement reminder
212
- console.print() # Empty line for spacing
213
- console.print(
214
- Panel(
215
- "[bold red]IMPORTANT:[/bold red] Project configuration updated successfully.\n\n"
216
- "[yellow]You must manually move your project files from the old location to:[/yellow]\n"
217
- f"[cyan]{resolved_path}[/cyan]\n\n"
218
- "[dim]Basic Memory has only updated the configuration - your files remain in their original location.[/dim]",
219
- title="⚠️ Manual File Movement Required",
220
- border_style="yellow",
221
- expand=False,
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
- except Exception as e:
226
- console.print(f"[red]Error moving project: {str(e)}[/red]")
227
- raise typer.Exit(1)
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")
@@ -41,6 +41,10 @@ class CloudProjectCreateRequest(BaseModel):
41
41
  class CloudProjectCreateResponse(BaseModel):
42
42
  """Response from creating a cloud project."""
43
43
 
44
- name: str = Field(..., description="Created project name")
45
- path: str = Field(..., description="Created project path")
46
- message: str = Field(default="", description="Success message")
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.1
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=U6DAt7BGs4nUoZawTqHojv50O6CP7oVBXoEIkaeKGcw,256
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=CJF-Dp69EPNNDwxZUakEiUcn-qCiEdMmHD3vNkO8pr0,13738
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=lxYVkUnYzg4GWByHmSm77m_a-ArOc_-4qvVvl0oPNp8,255
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=9uTQvDcqUj8mSMOMtpI_6UzVVWFb2oVWlueTc-dX7j0,28626
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=Y8rqZHOM16xsW_kYVqKB3AxG5ig32KWLHHzFulAjwBc,1566
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.1.dist-info/METADATA,sha256=Qg0Ljui70r0LXBf9iHJ2CQCCY6TukdOcz-DWneGlyeI,15670
143
- basic_memory-0.15.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
144
- basic_memory-0.15.1.dist-info/entry_points.txt,sha256=wvE2mRF6-Pg4weIYcfQ-86NOLZD4WJg7F7TIsRVFLb8,90
145
- basic_memory-0.15.1.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
146
- basic_memory-0.15.1.dist-info/RECORD,,
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,,