basic-memory 0.12.3__py3-none-any.whl → 0.13.0b1__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 +7 -1
- basic_memory/alembic/env.py +1 -1
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -5
- basic_memory/api/app.py +43 -13
- basic_memory/api/routers/__init__.py +4 -2
- basic_memory/api/routers/directory_router.py +63 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +127 -38
- basic_memory/api/routers/management_router.py +78 -0
- basic_memory/api/routers/memory_router.py +4 -59
- basic_memory/api/routers/project_router.py +230 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/search_router.py +3 -21
- basic_memory/api/routers/utils.py +130 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/cli/app.py +20 -21
- basic_memory/cli/commands/__init__.py +2 -1
- basic_memory/cli/commands/auth.py +136 -0
- basic_memory/cli/commands/db.py +3 -3
- basic_memory/cli/commands/import_chatgpt.py +31 -207
- basic_memory/cli/commands/import_claude_conversations.py +16 -142
- basic_memory/cli/commands/import_claude_projects.py +33 -143
- basic_memory/cli/commands/import_memory_json.py +26 -83
- basic_memory/cli/commands/mcp.py +71 -18
- basic_memory/cli/commands/project.py +99 -67
- basic_memory/cli/commands/status.py +19 -9
- basic_memory/cli/commands/sync.py +44 -58
- basic_memory/cli/main.py +1 -5
- basic_memory/config.py +145 -88
- basic_memory/db.py +6 -4
- basic_memory/deps.py +227 -30
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +222 -0
- basic_memory/importers/claude_conversations_importer.py +172 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +93 -0
- basic_memory/importers/utils.py +58 -0
- basic_memory/markdown/entity_parser.py +5 -2
- basic_memory/mcp/auth_provider.py +270 -0
- basic_memory/mcp/external_auth_provider.py +321 -0
- basic_memory/mcp/project_session.py +103 -0
- basic_memory/mcp/prompts/continue_conversation.py +18 -68
- basic_memory/mcp/prompts/recent_activity.py +19 -3
- basic_memory/mcp/prompts/search.py +14 -140
- basic_memory/mcp/prompts/utils.py +3 -3
- basic_memory/mcp/{tools → resources}/project_info.py +6 -2
- basic_memory/mcp/server.py +82 -8
- basic_memory/mcp/supabase_auth_provider.py +463 -0
- basic_memory/mcp/tools/__init__.py +20 -0
- basic_memory/mcp/tools/build_context.py +11 -1
- basic_memory/mcp/tools/canvas.py +15 -2
- basic_memory/mcp/tools/delete_note.py +12 -4
- basic_memory/mcp/tools/edit_note.py +297 -0
- basic_memory/mcp/tools/list_directory.py +154 -0
- basic_memory/mcp/tools/move_note.py +87 -0
- basic_memory/mcp/tools/project_management.py +300 -0
- basic_memory/mcp/tools/read_content.py +15 -6
- basic_memory/mcp/tools/read_note.py +17 -5
- basic_memory/mcp/tools/recent_activity.py +11 -2
- basic_memory/mcp/tools/search.py +10 -1
- basic_memory/mcp/tools/utils.py +137 -12
- basic_memory/mcp/tools/write_note.py +11 -15
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +16 -4
- basic_memory/models/project.py +80 -0
- basic_memory/models/search.py +8 -5
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +8 -3
- basic_memory/repository/observation_repository.py +35 -3
- basic_memory/repository/project_info_repository.py +3 -2
- basic_memory/repository/project_repository.py +85 -0
- basic_memory/repository/relation_repository.py +8 -2
- basic_memory/repository/repository.py +107 -15
- basic_memory/repository/search_repository.py +87 -27
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +34 -0
- basic_memory/schemas/memory.py +26 -12
- basic_memory/schemas/project_info.py +112 -2
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +56 -2
- basic_memory/schemas/search.py +1 -1
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +208 -95
- basic_memory/services/directory_service.py +167 -0
- basic_memory/services/entity_service.py +385 -5
- basic_memory/services/exceptions.py +6 -0
- basic_memory/services/file_service.py +14 -15
- basic_memory/services/initialization.py +144 -67
- basic_memory/services/link_resolver.py +16 -8
- basic_memory/services/project_service.py +548 -0
- basic_memory/services/search_service.py +77 -2
- basic_memory/sync/background_sync.py +25 -0
- basic_memory/sync/sync_service.py +10 -9
- basic_memory/sync/watch_service.py +63 -39
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/METADATA +23 -1
- basic_memory-0.13.0b1.dist-info/RECORD +132 -0
- basic_memory/api/routers/project_info_router.py +0 -274
- basic_memory/mcp/main.py +0 -24
- basic_memory-0.12.3.dist-info/RECORD +0 -100
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/WHEEL +0 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,94 +3,20 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import Annotated
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
|
-
from loguru import logger
|
|
10
|
-
from rich.console import Console
|
|
11
|
-
from rich.panel import Panel
|
|
12
|
-
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
13
|
-
|
|
14
9
|
from basic_memory.cli.app import import_app
|
|
15
10
|
from basic_memory.config import config
|
|
11
|
+
from basic_memory.importers.memory_json_importer import MemoryJsonImporter
|
|
16
12
|
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
|
17
|
-
from
|
|
13
|
+
from loguru import logger
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.panel import Panel
|
|
18
16
|
|
|
19
17
|
console = Console()
|
|
20
18
|
|
|
21
19
|
|
|
22
|
-
async def process_memory_json(
|
|
23
|
-
json_path: Path, base_path: Path, markdown_processor: MarkdownProcessor
|
|
24
|
-
):
|
|
25
|
-
"""Import entities from memory.json using markdown processor."""
|
|
26
|
-
|
|
27
|
-
# First pass - collect all relations by source entity
|
|
28
|
-
entity_relations: Dict[str, List[Relation]] = {}
|
|
29
|
-
entities: Dict[str, Dict[str, Any]] = {}
|
|
30
|
-
|
|
31
|
-
with Progress(
|
|
32
|
-
SpinnerColumn(),
|
|
33
|
-
TextColumn("[progress.description]{task.description}"),
|
|
34
|
-
BarColumn(),
|
|
35
|
-
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
36
|
-
console=console,
|
|
37
|
-
) as progress:
|
|
38
|
-
read_task = progress.add_task("Reading memory.json...", total=None)
|
|
39
|
-
|
|
40
|
-
# First pass - collect entities and relations
|
|
41
|
-
with open(json_path, encoding="utf-8") as f:
|
|
42
|
-
lines = f.readlines()
|
|
43
|
-
progress.update(read_task, total=len(lines))
|
|
44
|
-
|
|
45
|
-
for line in lines:
|
|
46
|
-
data = json.loads(line)
|
|
47
|
-
if data["type"] == "entity":
|
|
48
|
-
entities[data["name"]] = data
|
|
49
|
-
elif data["type"] == "relation":
|
|
50
|
-
# Store relation with its source entity
|
|
51
|
-
source = data.get("from") or data.get("from_id")
|
|
52
|
-
if source not in entity_relations:
|
|
53
|
-
entity_relations[source] = []
|
|
54
|
-
entity_relations[source].append(
|
|
55
|
-
Relation(
|
|
56
|
-
type=data.get("relationType") or data.get("relation_type"),
|
|
57
|
-
target=data.get("to") or data.get("to_id"),
|
|
58
|
-
)
|
|
59
|
-
)
|
|
60
|
-
progress.update(read_task, advance=1)
|
|
61
|
-
|
|
62
|
-
# Second pass - create and write entities
|
|
63
|
-
write_task = progress.add_task("Creating entities...", total=len(entities))
|
|
64
|
-
|
|
65
|
-
entities_created = 0
|
|
66
|
-
for name, entity_data in entities.items():
|
|
67
|
-
entity = EntityMarkdown(
|
|
68
|
-
frontmatter=EntityFrontmatter(
|
|
69
|
-
metadata={
|
|
70
|
-
"type": entity_data["entityType"],
|
|
71
|
-
"title": name,
|
|
72
|
-
"permalink": f"{entity_data['entityType']}/{name}",
|
|
73
|
-
}
|
|
74
|
-
),
|
|
75
|
-
content=f"# {name}\n",
|
|
76
|
-
observations=[Observation(content=obs) for obs in entity_data["observations"]],
|
|
77
|
-
relations=entity_relations.get(
|
|
78
|
-
name, []
|
|
79
|
-
), # Add any relations where this entity is the source
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
# Let markdown processor handle writing
|
|
83
|
-
file_path = base_path / f"{entity_data['entityType']}/{name}.md"
|
|
84
|
-
await markdown_processor.write_file(file_path, entity)
|
|
85
|
-
entities_created += 1
|
|
86
|
-
progress.update(write_task, advance=1)
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
"entities": entities_created,
|
|
90
|
-
"relations": sum(len(rels) for rels in entity_relations.values()),
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
20
|
async def get_markdown_processor() -> MarkdownProcessor:
|
|
95
21
|
"""Get MarkdownProcessor instance."""
|
|
96
22
|
entity_parser = EntityParser(config.home)
|
|
@@ -102,6 +28,9 @@ def memory_json(
|
|
|
102
28
|
json_path: Annotated[Path, typer.Argument(..., help="Path to memory.json file")] = Path(
|
|
103
29
|
"memory.json"
|
|
104
30
|
),
|
|
31
|
+
destination_folder: Annotated[
|
|
32
|
+
str, typer.Option(help="Optional destination folder within the project")
|
|
33
|
+
] = "",
|
|
105
34
|
):
|
|
106
35
|
"""Import entities and relations from a memory.json file.
|
|
107
36
|
|
|
@@ -121,17 +50,31 @@ def memory_json(
|
|
|
121
50
|
# Get markdown processor
|
|
122
51
|
markdown_processor = asyncio.run(get_markdown_processor())
|
|
123
52
|
|
|
53
|
+
# Create the importer
|
|
54
|
+
importer = MemoryJsonImporter(config.home, markdown_processor)
|
|
55
|
+
|
|
124
56
|
# Process the file
|
|
125
|
-
base_path = config.home
|
|
57
|
+
base_path = config.home if not destination_folder else config.home / destination_folder
|
|
126
58
|
console.print(f"\nImporting from {json_path}...writing to {base_path}")
|
|
127
|
-
|
|
59
|
+
|
|
60
|
+
# Run the import for json log format
|
|
61
|
+
file_data = []
|
|
62
|
+
with json_path.open("r", encoding="utf-8") as file:
|
|
63
|
+
for line in file:
|
|
64
|
+
json_data = json.loads(line)
|
|
65
|
+
file_data.append(json_data)
|
|
66
|
+
result = asyncio.run(importer.import_data(file_data, destination_folder))
|
|
67
|
+
|
|
68
|
+
if not result.success: # pragma: no cover
|
|
69
|
+
typer.echo(f"Error during import: {result.error_message}", err=True)
|
|
70
|
+
raise typer.Exit(1)
|
|
128
71
|
|
|
129
72
|
# Show results
|
|
130
73
|
console.print(
|
|
131
74
|
Panel(
|
|
132
75
|
f"[green]Import complete![/green]\n\n"
|
|
133
|
-
f"Created {
|
|
134
|
-
f"Added {
|
|
76
|
+
f"Created {result.entities} entities\n"
|
|
77
|
+
f"Added {result.relations} relations",
|
|
135
78
|
expand=False,
|
|
136
79
|
)
|
|
137
80
|
)
|
basic_memory/cli/commands/mcp.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
"""MCP server command."""
|
|
1
|
+
"""MCP server command with streamable HTTP transport."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import typer
|
|
2
5
|
|
|
3
|
-
import basic_memory
|
|
4
6
|
from basic_memory.cli.app import app
|
|
5
7
|
|
|
6
8
|
# Import mcp instance
|
|
@@ -9,27 +11,78 @@ from basic_memory.mcp.server import mcp as mcp_server # pragma: no cover
|
|
|
9
11
|
# Import mcp tools to register them
|
|
10
12
|
import basic_memory.mcp.tools # noqa: F401 # pragma: no cover
|
|
11
13
|
|
|
14
|
+
# Import prompts to register them
|
|
15
|
+
import basic_memory.mcp.prompts # noqa: F401 # pragma: no cover
|
|
16
|
+
from loguru import logger
|
|
17
|
+
|
|
12
18
|
|
|
13
19
|
@app.command()
|
|
14
|
-
def mcp(
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
def mcp(
|
|
21
|
+
transport: str = typer.Option("stdio", help="Transport type: stdio, streamable-http, or sse"),
|
|
22
|
+
host: str = typer.Option(
|
|
23
|
+
"0.0.0.0", help="Host for HTTP transports (use 0.0.0.0 to allow external connections)"
|
|
24
|
+
),
|
|
25
|
+
port: int = typer.Option(8000, help="Port for HTTP transports"),
|
|
26
|
+
path: str = typer.Option("/mcp", help="Path prefix for streamable-http transport"),
|
|
27
|
+
): # pragma: no cover
|
|
28
|
+
"""Run the MCP server with configurable transport options.
|
|
29
|
+
|
|
30
|
+
This command starts an MCP server using one of three transport options:
|
|
31
|
+
|
|
32
|
+
- stdio: Standard I/O (good for local usage)
|
|
33
|
+
- streamable-http: Recommended for web deployments (default)
|
|
34
|
+
- sse: Server-Sent Events (for compatibility with existing clients)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
# Check if OAuth is enabled
|
|
38
|
+
import os
|
|
39
|
+
|
|
40
|
+
auth_enabled = os.getenv("FASTMCP_AUTH_ENABLED", "false").lower() == "true"
|
|
41
|
+
if auth_enabled:
|
|
42
|
+
logger.info("OAuth authentication is ENABLED")
|
|
43
|
+
logger.info(f"Issuer URL: {os.getenv('FASTMCP_AUTH_ISSUER_URL', 'http://localhost:8000')}")
|
|
44
|
+
if os.getenv("FASTMCP_AUTH_REQUIRED_SCOPES"):
|
|
45
|
+
logger.info(f"Required scopes: {os.getenv('FASTMCP_AUTH_REQUIRED_SCOPES')}")
|
|
46
|
+
else:
|
|
47
|
+
logger.info("OAuth authentication is DISABLED")
|
|
48
|
+
|
|
49
|
+
from basic_memory.config import app_config
|
|
50
|
+
from basic_memory.services.initialization import initialize_file_sync
|
|
19
51
|
|
|
20
|
-
#
|
|
21
|
-
asyncio.run(initialize_database(config))
|
|
52
|
+
# Start the MCP server with the specified transport
|
|
22
53
|
|
|
23
|
-
#
|
|
24
|
-
|
|
54
|
+
# Use unified thread-based sync approach for both transports
|
|
55
|
+
import threading
|
|
25
56
|
|
|
26
|
-
|
|
57
|
+
def run_file_sync():
|
|
58
|
+
"""Run file sync in a separate thread with its own event loop."""
|
|
59
|
+
loop = asyncio.new_event_loop()
|
|
60
|
+
asyncio.set_event_loop(loop)
|
|
61
|
+
try:
|
|
62
|
+
loop.run_until_complete(initialize_file_sync(app_config))
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error(f"File sync error: {e}", err=True)
|
|
65
|
+
finally:
|
|
66
|
+
loop.close()
|
|
27
67
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
68
|
+
logger.info(f"Sync changes enabled: {app_config.sync_changes}")
|
|
69
|
+
if app_config.sync_changes:
|
|
70
|
+
# Start the sync thread
|
|
71
|
+
sync_thread = threading.Thread(target=run_file_sync, daemon=True)
|
|
72
|
+
sync_thread.start()
|
|
73
|
+
logger.info("Started file sync in background")
|
|
31
74
|
|
|
32
|
-
|
|
75
|
+
# Now run the MCP server (blocks)
|
|
76
|
+
logger.info(f"Starting MCP server with {transport.upper()} transport")
|
|
33
77
|
|
|
34
|
-
|
|
35
|
-
|
|
78
|
+
if transport == "stdio":
|
|
79
|
+
mcp_server.run(
|
|
80
|
+
transport=transport,
|
|
81
|
+
)
|
|
82
|
+
elif transport == "streamable-http" or transport == "sse":
|
|
83
|
+
mcp_server.run(
|
|
84
|
+
transport=transport,
|
|
85
|
+
host=host,
|
|
86
|
+
port=port,
|
|
87
|
+
path=path,
|
|
88
|
+
)
|
|
@@ -9,13 +9,21 @@ from rich.console import Console
|
|
|
9
9
|
from rich.table import Table
|
|
10
10
|
|
|
11
11
|
from basic_memory.cli.app import app
|
|
12
|
-
from basic_memory.config import
|
|
13
|
-
from basic_memory.mcp.
|
|
12
|
+
from basic_memory.config import config
|
|
13
|
+
from basic_memory.mcp.project_session import session
|
|
14
|
+
from basic_memory.mcp.resources.project_info import project_info
|
|
14
15
|
import json
|
|
15
16
|
from datetime import datetime
|
|
16
17
|
|
|
17
18
|
from rich.panel import Panel
|
|
18
19
|
from rich.tree import Tree
|
|
20
|
+
from basic_memory.mcp.async_client import client
|
|
21
|
+
from basic_memory.mcp.tools.utils import call_get
|
|
22
|
+
from basic_memory.schemas.project_info import ProjectList
|
|
23
|
+
from basic_memory.mcp.tools.utils import call_post
|
|
24
|
+
from basic_memory.schemas.project_info import ProjectStatusResponse
|
|
25
|
+
from basic_memory.mcp.tools.utils import call_delete
|
|
26
|
+
from basic_memory.mcp.tools.utils import call_put
|
|
19
27
|
|
|
20
28
|
console = Console()
|
|
21
29
|
|
|
@@ -28,112 +36,135 @@ def format_path(path: str) -> str:
|
|
|
28
36
|
"""Format a path for display, using ~ for home directory."""
|
|
29
37
|
home = str(Path.home())
|
|
30
38
|
if path.startswith(home):
|
|
31
|
-
return path.replace(home, "~", 1)
|
|
39
|
+
return path.replace(home, "~", 1) # pragma: no cover
|
|
32
40
|
return path
|
|
33
41
|
|
|
34
42
|
|
|
35
43
|
@project_app.command("list")
|
|
36
44
|
def list_projects() -> None:
|
|
37
45
|
"""List all configured projects."""
|
|
38
|
-
|
|
39
|
-
projects = config_manager.projects
|
|
46
|
+
# Use API to list projects
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
table.add_column("Name", style="cyan")
|
|
43
|
-
table.add_column("Path", style="green")
|
|
44
|
-
table.add_column("Default", style="yellow")
|
|
45
|
-
table.add_column("Active", style="magenta")
|
|
48
|
+
project_url = config.project_url
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
table.
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
try:
|
|
51
|
+
response = asyncio.run(call_get(client, f"{project_url}/project/projects"))
|
|
52
|
+
result = ProjectList.model_validate(response.json())
|
|
53
|
+
|
|
54
|
+
table = Table(title="Basic Memory Projects")
|
|
55
|
+
table.add_column("Name", style="cyan")
|
|
56
|
+
table.add_column("Path", style="green")
|
|
57
|
+
table.add_column("Default", style="yellow")
|
|
58
|
+
table.add_column("Active", style="magenta")
|
|
59
|
+
|
|
60
|
+
for project in result.projects:
|
|
61
|
+
is_default = "✓" if project.is_default else ""
|
|
62
|
+
is_active = "✓" if session.get_current_project() == project.name else ""
|
|
63
|
+
table.add_row(project.name, format_path(project.path), is_default, is_active)
|
|
64
|
+
|
|
65
|
+
console.print(table)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
console.print(f"[red]Error listing projects: {str(e)}[/red]")
|
|
68
|
+
console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]")
|
|
69
|
+
raise typer.Exit(1)
|
|
56
70
|
|
|
57
71
|
|
|
58
72
|
@project_app.command("add")
|
|
59
73
|
def add_project(
|
|
60
74
|
name: str = typer.Argument(..., help="Name of the project"),
|
|
61
75
|
path: str = typer.Argument(..., help="Path to the project directory"),
|
|
76
|
+
set_default: bool = typer.Option(False, "--default", help="Set as default project"),
|
|
62
77
|
) -> None:
|
|
63
78
|
"""Add a new project."""
|
|
64
|
-
|
|
79
|
+
# Resolve to absolute path
|
|
80
|
+
resolved_path = os.path.abspath(os.path.expanduser(path))
|
|
65
81
|
|
|
66
82
|
try:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
console.print("
|
|
74
|
-
|
|
75
|
-
console.print("
|
|
76
|
-
console.print(
|
|
77
|
-
except ValueError as e:
|
|
78
|
-
console.print(f"[red]Error: {e}[/red]")
|
|
83
|
+
project_url = config.project_url
|
|
84
|
+
data = {"name": name, "path": resolved_path, "set_default": set_default}
|
|
85
|
+
|
|
86
|
+
response = asyncio.run(call_post(client, f"{project_url}/project/projects", json=data))
|
|
87
|
+
result = ProjectStatusResponse.model_validate(response.json())
|
|
88
|
+
|
|
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
|
+
console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]")
|
|
79
93
|
raise typer.Exit(1)
|
|
80
94
|
|
|
95
|
+
# Display usage hint
|
|
96
|
+
console.print("\nTo use this project:")
|
|
97
|
+
console.print(f" basic-memory --project={name} <command>")
|
|
98
|
+
console.print(" # or")
|
|
99
|
+
console.print(f" basic-memory project default {name}")
|
|
100
|
+
|
|
81
101
|
|
|
82
102
|
@project_app.command("remove")
|
|
83
103
|
def remove_project(
|
|
84
104
|
name: str = typer.Argument(..., help="Name of the project to remove"),
|
|
85
105
|
) -> None:
|
|
86
106
|
"""Remove a project from configuration."""
|
|
87
|
-
config_manager = ConfigManager()
|
|
88
|
-
|
|
89
107
|
try:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
project_url = config.project_url
|
|
109
|
+
|
|
110
|
+
response = asyncio.run(call_delete(client, f"{project_url}/project/projects/{name}"))
|
|
111
|
+
result = ProjectStatusResponse.model_validate(response.json())
|
|
112
|
+
|
|
113
|
+
console.print(f"[green]{result.message}[/green]")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
console.print(f"[red]Error removing project: {str(e)}[/red]")
|
|
116
|
+
console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]")
|
|
95
117
|
raise typer.Exit(1)
|
|
96
118
|
|
|
119
|
+
# Show this message regardless of method used
|
|
120
|
+
console.print("[yellow]Note: The project files have not been deleted from disk.[/yellow]")
|
|
121
|
+
|
|
97
122
|
|
|
98
123
|
@project_app.command("default")
|
|
99
124
|
def set_default_project(
|
|
100
125
|
name: str = typer.Argument(..., help="Name of the project to set as default"),
|
|
101
126
|
) -> None:
|
|
102
127
|
"""Set the default project and activate it for the current session."""
|
|
103
|
-
config_manager = ConfigManager()
|
|
104
|
-
|
|
105
128
|
try:
|
|
106
|
-
|
|
107
|
-
config_manager.set_default_project(name)
|
|
108
|
-
|
|
109
|
-
# Also activate it for the current session by setting the environment variable
|
|
110
|
-
os.environ["BASIC_MEMORY_PROJECT"] = name
|
|
129
|
+
project_url = config.project_url
|
|
111
130
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
from basic_memory import config as config_module
|
|
131
|
+
response = asyncio.run(call_put(client, f"{project_url}/project/projects/{name}/default"))
|
|
132
|
+
result = ProjectStatusResponse.model_validate(response.json())
|
|
115
133
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
console.print(
|
|
134
|
+
console.print(f"[green]{result.message}[/green]")
|
|
135
|
+
except Exception as e:
|
|
136
|
+
console.print(f"[red]Error setting default project: {str(e)}[/red]")
|
|
137
|
+
console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]")
|
|
120
138
|
raise typer.Exit(1)
|
|
121
139
|
|
|
140
|
+
# Always activate it for the current session
|
|
141
|
+
os.environ["BASIC_MEMORY_PROJECT"] = name
|
|
142
|
+
|
|
143
|
+
# Reload configuration to apply the change
|
|
144
|
+
from importlib import reload
|
|
145
|
+
from basic_memory import config as config_module
|
|
146
|
+
|
|
147
|
+
reload(config_module)
|
|
148
|
+
|
|
149
|
+
console.print("[green]Project activated for current session[/green]")
|
|
122
150
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
151
|
+
|
|
152
|
+
@project_app.command("sync")
|
|
153
|
+
def synchronize_projects() -> None:
|
|
154
|
+
"""Synchronize projects between configuration file and database."""
|
|
155
|
+
# Call the API to synchronize projects
|
|
156
|
+
|
|
157
|
+
project_url = config.project_url
|
|
128
158
|
|
|
129
159
|
try:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
console.print(f"
|
|
134
|
-
except
|
|
135
|
-
console.print(f"[
|
|
136
|
-
console.print(
|
|
160
|
+
response = asyncio.run(call_post(client, f"{project_url}/project/sync"))
|
|
161
|
+
result = ProjectStatusResponse.model_validate(response.json())
|
|
162
|
+
|
|
163
|
+
console.print(f"[green]{result.message}[/green]")
|
|
164
|
+
except Exception as e: # pragma: no cover
|
|
165
|
+
console.print(f"[red]Error synchronizing projects: {str(e)}[/red]")
|
|
166
|
+
console.print("[yellow]Note: Make sure the Basic Memory server is running.[/yellow]")
|
|
167
|
+
raise typer.Exit(1)
|
|
137
168
|
|
|
138
169
|
|
|
139
170
|
@project_app.command("info")
|
|
@@ -266,9 +297,10 @@ def display_project_info(
|
|
|
266
297
|
projects_table.add_column("Path", style="cyan")
|
|
267
298
|
projects_table.add_column("Default", style="green")
|
|
268
299
|
|
|
269
|
-
for name,
|
|
300
|
+
for name, proj_info in info.available_projects.items():
|
|
270
301
|
is_default = name == info.default_project
|
|
271
|
-
|
|
302
|
+
project_path = proj_info["path"]
|
|
303
|
+
projects_table.add_row(name, project_path, "✓" if is_default else "")
|
|
272
304
|
|
|
273
305
|
console.print(projects_table)
|
|
274
306
|
|
|
@@ -9,10 +9,11 @@ from rich.console import Console
|
|
|
9
9
|
from rich.panel import Panel
|
|
10
10
|
from rich.tree import Tree
|
|
11
11
|
|
|
12
|
+
from basic_memory import db
|
|
12
13
|
from basic_memory.cli.app import app
|
|
13
14
|
from basic_memory.cli.commands.sync import get_sync_service
|
|
14
|
-
from basic_memory.config import config
|
|
15
|
-
from basic_memory.
|
|
15
|
+
from basic_memory.config import config, app_config
|
|
16
|
+
from basic_memory.repository import ProjectRepository
|
|
16
17
|
from basic_memory.sync.sync_service import SyncReport
|
|
17
18
|
|
|
18
19
|
# Create rich console
|
|
@@ -86,9 +87,9 @@ def build_directory_summary(counts: Dict[str, int]) -> str:
|
|
|
86
87
|
return " ".join(parts)
|
|
87
88
|
|
|
88
89
|
|
|
89
|
-
def display_changes(title: str, changes: SyncReport, verbose: bool = False):
|
|
90
|
+
def display_changes(project_name: str, title: str, changes: SyncReport, verbose: bool = False):
|
|
90
91
|
"""Display changes using Rich for better visualization."""
|
|
91
|
-
tree = Tree(title)
|
|
92
|
+
tree = Tree(f"{project_name}: {title}")
|
|
92
93
|
|
|
93
94
|
if changes.total == 0:
|
|
94
95
|
tree.add("No changes")
|
|
@@ -121,11 +122,21 @@ def display_changes(title: str, changes: SyncReport, verbose: bool = False):
|
|
|
121
122
|
console.print(Panel(tree, expand=False))
|
|
122
123
|
|
|
123
124
|
|
|
124
|
-
async def run_status(
|
|
125
|
+
async def run_status(verbose: bool = False):
|
|
125
126
|
"""Check sync status of files vs database."""
|
|
126
127
|
# Check knowledge/ directory
|
|
128
|
+
|
|
129
|
+
_, session_maker = await db.get_or_create_db(
|
|
130
|
+
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
|
131
|
+
)
|
|
132
|
+
project_repository = ProjectRepository(session_maker)
|
|
133
|
+
project = await project_repository.get_by_name(config.project)
|
|
134
|
+
if not project: # pragma: no cover
|
|
135
|
+
raise Exception(f"Project '{config.project}' not found")
|
|
136
|
+
|
|
137
|
+
sync_service = await get_sync_service(project)
|
|
127
138
|
knowledge_changes = await sync_service.scan(config.home)
|
|
128
|
-
display_changes("Status", knowledge_changes, verbose)
|
|
139
|
+
display_changes(project.name, "Status", knowledge_changes, verbose)
|
|
129
140
|
|
|
130
141
|
|
|
131
142
|
@app.command()
|
|
@@ -134,9 +145,8 @@ def status(
|
|
|
134
145
|
):
|
|
135
146
|
"""Show sync status between files and database."""
|
|
136
147
|
try:
|
|
137
|
-
|
|
138
|
-
asyncio.run(run_status(sync_service, verbose)) # pragma: no cover
|
|
148
|
+
asyncio.run(run_status(verbose)) # pragma: no cover
|
|
139
149
|
except Exception as e:
|
|
140
|
-
logger.
|
|
150
|
+
logger.error(f"Error checking status: {e}")
|
|
141
151
|
typer.echo(f"Error checking status: {e}", err=True)
|
|
142
152
|
raise typer.Exit(code=1) # pragma: no cover
|