basic-memory 0.7.0__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of basic-memory might be problematic. Click here for more details.
- basic_memory/__init__.py +1 -1
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +23 -1
- basic_memory/alembic/migrations.py +4 -9
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +106 -0
- basic_memory/api/app.py +9 -10
- basic_memory/api/routers/__init__.py +2 -1
- basic_memory/api/routers/knowledge_router.py +31 -5
- basic_memory/api/routers/memory_router.py +18 -17
- basic_memory/api/routers/project_info_router.py +275 -0
- basic_memory/api/routers/resource_router.py +105 -4
- basic_memory/api/routers/search_router.py +22 -4
- basic_memory/cli/app.py +54 -5
- basic_memory/cli/commands/__init__.py +15 -2
- basic_memory/cli/commands/db.py +9 -13
- basic_memory/cli/commands/import_chatgpt.py +26 -30
- basic_memory/cli/commands/import_claude_conversations.py +27 -29
- basic_memory/cli/commands/import_claude_projects.py +29 -31
- basic_memory/cli/commands/import_memory_json.py +26 -28
- basic_memory/cli/commands/mcp.py +7 -1
- basic_memory/cli/commands/project.py +119 -0
- basic_memory/cli/commands/project_info.py +167 -0
- basic_memory/cli/commands/status.py +14 -28
- basic_memory/cli/commands/sync.py +63 -22
- basic_memory/cli/commands/tool.py +253 -0
- basic_memory/cli/main.py +39 -1
- basic_memory/config.py +166 -4
- basic_memory/db.py +19 -4
- basic_memory/deps.py +10 -3
- basic_memory/file_utils.py +37 -19
- basic_memory/markdown/entity_parser.py +3 -3
- basic_memory/markdown/utils.py +5 -0
- basic_memory/mcp/async_client.py +1 -1
- basic_memory/mcp/main.py +24 -0
- basic_memory/mcp/prompts/__init__.py +19 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +26 -0
- basic_memory/mcp/prompts/continue_conversation.py +111 -0
- basic_memory/mcp/prompts/recent_activity.py +88 -0
- basic_memory/mcp/prompts/search.py +182 -0
- basic_memory/mcp/prompts/utils.py +155 -0
- basic_memory/mcp/server.py +2 -6
- basic_memory/mcp/tools/__init__.py +12 -21
- basic_memory/mcp/tools/build_context.py +85 -0
- basic_memory/mcp/tools/canvas.py +97 -0
- basic_memory/mcp/tools/delete_note.py +28 -0
- basic_memory/mcp/tools/project_info.py +51 -0
- basic_memory/mcp/tools/read_content.py +229 -0
- basic_memory/mcp/tools/read_note.py +190 -0
- basic_memory/mcp/tools/recent_activity.py +100 -0
- basic_memory/mcp/tools/search.py +56 -17
- basic_memory/mcp/tools/utils.py +245 -16
- basic_memory/mcp/tools/write_note.py +124 -0
- basic_memory/models/knowledge.py +27 -11
- basic_memory/models/search.py +2 -1
- basic_memory/repository/entity_repository.py +3 -2
- basic_memory/repository/project_info_repository.py +9 -0
- basic_memory/repository/repository.py +24 -7
- basic_memory/repository/search_repository.py +47 -14
- basic_memory/schemas/__init__.py +10 -9
- basic_memory/schemas/base.py +4 -1
- basic_memory/schemas/memory.py +14 -4
- basic_memory/schemas/project_info.py +96 -0
- basic_memory/schemas/search.py +29 -33
- basic_memory/services/context_service.py +3 -3
- basic_memory/services/entity_service.py +26 -13
- basic_memory/services/file_service.py +145 -26
- basic_memory/services/link_resolver.py +9 -46
- basic_memory/services/search_service.py +95 -22
- basic_memory/sync/__init__.py +3 -2
- basic_memory/sync/sync_service.py +523 -117
- basic_memory/sync/watch_service.py +258 -132
- basic_memory/utils.py +51 -36
- basic_memory-0.9.0.dist-info/METADATA +736 -0
- basic_memory-0.9.0.dist-info/RECORD +99 -0
- basic_memory/alembic/README +0 -1
- basic_memory/cli/commands/tools.py +0 -157
- basic_memory/mcp/tools/knowledge.py +0 -68
- basic_memory/mcp/tools/memory.py +0 -170
- basic_memory/mcp/tools/notes.py +0 -202
- basic_memory/schemas/discovery.py +0 -28
- basic_memory/sync/file_change_scanner.py +0 -158
- basic_memory/sync/utils.py +0 -31
- basic_memory-0.7.0.dist-info/METADATA +0 -378
- basic_memory-0.7.0.dist-info/RECORD +0 -82
- {basic_memory-0.7.0.dist-info → basic_memory-0.9.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.7.0.dist-info → basic_memory-0.9.0.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.7.0.dist-info → basic_memory-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,7 +6,6 @@ from datetime import datetime
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Dict, Any, List, Annotated
|
|
8
8
|
|
|
9
|
-
import logfire
|
|
10
9
|
import typer
|
|
11
10
|
from loguru import logger
|
|
12
11
|
from rich.console import Console
|
|
@@ -179,35 +178,34 @@ def import_claude(
|
|
|
179
178
|
After importing, run 'basic-memory sync' to index the new files.
|
|
180
179
|
"""
|
|
181
180
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
raise typer.Exit(1)
|
|
187
|
-
|
|
188
|
-
# Get markdown processor
|
|
189
|
-
markdown_processor = asyncio.run(get_markdown_processor())
|
|
190
|
-
|
|
191
|
-
# Process the file
|
|
192
|
-
base_path = config.home / folder
|
|
193
|
-
console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
|
|
194
|
-
results = asyncio.run(
|
|
195
|
-
process_conversations_json(conversations_json, base_path, markdown_processor)
|
|
196
|
-
)
|
|
181
|
+
try:
|
|
182
|
+
if not conversations_json.exists():
|
|
183
|
+
typer.echo(f"Error: File not found: {conversations_json}", err=True)
|
|
184
|
+
raise typer.Exit(1)
|
|
197
185
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
186
|
+
# Get markdown processor
|
|
187
|
+
markdown_processor = asyncio.run(get_markdown_processor())
|
|
188
|
+
|
|
189
|
+
# Process the file
|
|
190
|
+
base_path = config.home / folder
|
|
191
|
+
console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
|
|
192
|
+
results = asyncio.run(
|
|
193
|
+
process_conversations_json(conversations_json, base_path, markdown_processor)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Show results
|
|
197
|
+
console.print(
|
|
198
|
+
Panel(
|
|
199
|
+
f"[green]Import complete![/green]\n\n"
|
|
200
|
+
f"Imported {results['conversations']} conversations\n"
|
|
201
|
+
f"Containing {results['messages']} messages",
|
|
202
|
+
expand=False,
|
|
206
203
|
)
|
|
204
|
+
)
|
|
207
205
|
|
|
208
|
-
|
|
206
|
+
console.print("\nRun 'basic-memory sync' to index the new files.")
|
|
209
207
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.error("Import failed")
|
|
210
|
+
typer.echo(f"Error during import: {e}", err=True)
|
|
211
|
+
raise typer.Exit(1)
|
|
@@ -5,7 +5,6 @@ import json
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Dict, Any, Annotated, Optional
|
|
7
7
|
|
|
8
|
-
import logfire
|
|
9
8
|
import typer
|
|
10
9
|
from loguru import logger
|
|
11
10
|
from rich.console import Console
|
|
@@ -161,36 +160,35 @@ def import_projects(
|
|
|
161
160
|
|
|
162
161
|
After importing, run 'basic-memory sync' to index the new files.
|
|
163
162
|
"""
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if projects_json:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
163
|
+
try:
|
|
164
|
+
if projects_json:
|
|
165
|
+
if not projects_json.exists():
|
|
166
|
+
typer.echo(f"Error: File not found: {projects_json}", err=True)
|
|
167
|
+
raise typer.Exit(1)
|
|
168
|
+
|
|
169
|
+
# Get markdown processor
|
|
170
|
+
markdown_processor = asyncio.run(get_markdown_processor())
|
|
171
|
+
|
|
172
|
+
# Process the file
|
|
173
|
+
base_path = config.home / base_folder if base_folder else config.home
|
|
174
|
+
console.print(f"\nImporting projects from {projects_json}...writing to {base_path}")
|
|
175
|
+
results = asyncio.run(
|
|
176
|
+
process_projects_json(projects_json, base_path, markdown_processor)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Show results
|
|
180
|
+
console.print(
|
|
181
|
+
Panel(
|
|
182
|
+
f"[green]Import complete![/green]\n\n"
|
|
183
|
+
f"Imported {results['documents']} project documents\n"
|
|
184
|
+
f"Imported {results['prompts']} prompt templates",
|
|
185
|
+
expand=False,
|
|
179
186
|
)
|
|
187
|
+
)
|
|
180
188
|
|
|
181
|
-
|
|
182
|
-
console.print(
|
|
183
|
-
Panel(
|
|
184
|
-
f"[green]Import complete![/green]\n\n"
|
|
185
|
-
f"Imported {results['documents']} project documents\n"
|
|
186
|
-
f"Imported {results['prompts']} prompt templates",
|
|
187
|
-
expand=False,
|
|
188
|
-
)
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
console.print("\nRun 'basic-memory sync' to index the new files.")
|
|
189
|
+
console.print("\nRun 'basic-memory sync' to index the new files.")
|
|
192
190
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.error("Import failed")
|
|
193
|
+
typer.echo(f"Error during import: {e}", err=True)
|
|
194
|
+
raise typer.Exit(1)
|
|
@@ -5,7 +5,6 @@ import json
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Dict, Any, List, Annotated
|
|
7
7
|
|
|
8
|
-
import logfire
|
|
9
8
|
import typer
|
|
10
9
|
from loguru import logger
|
|
11
10
|
from rich.console import Console
|
|
@@ -114,33 +113,32 @@ def memory_json(
|
|
|
114
113
|
After importing, run 'basic-memory sync' to index the new files.
|
|
115
114
|
"""
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
expand=False,
|
|
138
|
-
)
|
|
116
|
+
if not json_path.exists():
|
|
117
|
+
typer.echo(f"Error: File not found: {json_path}", err=True)
|
|
118
|
+
raise typer.Exit(1)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
# Get markdown processor
|
|
122
|
+
markdown_processor = asyncio.run(get_markdown_processor())
|
|
123
|
+
|
|
124
|
+
# Process the file
|
|
125
|
+
base_path = config.home
|
|
126
|
+
console.print(f"\nImporting from {json_path}...writing to {base_path}")
|
|
127
|
+
results = asyncio.run(process_memory_json(json_path, base_path, markdown_processor))
|
|
128
|
+
|
|
129
|
+
# Show results
|
|
130
|
+
console.print(
|
|
131
|
+
Panel(
|
|
132
|
+
f"[green]Import complete![/green]\n\n"
|
|
133
|
+
f"Created {results['entities']} entities\n"
|
|
134
|
+
f"Added {results['relations']} relations",
|
|
135
|
+
expand=False,
|
|
139
136
|
)
|
|
137
|
+
)
|
|
140
138
|
|
|
141
|
-
|
|
139
|
+
console.print("\nRun 'basic-memory sync' to index the new files.")
|
|
142
140
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error("Import failed")
|
|
143
|
+
typer.echo(f"Error during import: {e}", err=True)
|
|
144
|
+
raise typer.Exit(1)
|
basic_memory/cli/commands/mcp.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""MCP server command."""
|
|
2
2
|
|
|
3
3
|
from loguru import logger
|
|
4
|
+
|
|
5
|
+
import basic_memory
|
|
4
6
|
from basic_memory.cli.app import app
|
|
5
7
|
from basic_memory.config import config
|
|
6
8
|
|
|
@@ -15,6 +17,10 @@ import basic_memory.mcp.tools # noqa: F401 # pragma: no cover
|
|
|
15
17
|
def mcp(): # pragma: no cover
|
|
16
18
|
"""Run the MCP server for Claude Desktop integration."""
|
|
17
19
|
home_dir = config.home
|
|
20
|
+
project_name = config.project
|
|
21
|
+
|
|
18
22
|
logger.info(f"Starting Basic Memory MCP server {basic_memory.__version__}")
|
|
19
|
-
logger.info(f"
|
|
23
|
+
logger.info(f"Project: {project_name}")
|
|
24
|
+
logger.info(f"Project directory: {home_dir}")
|
|
25
|
+
|
|
20
26
|
mcp_server.run()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Command module for basic-memory project management."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from basic_memory.cli.app import app
|
|
11
|
+
from basic_memory.config import ConfigManager, config
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
# Create a project subcommand
|
|
16
|
+
project_app = typer.Typer(help="Manage multiple Basic Memory projects")
|
|
17
|
+
app.add_typer(project_app, name="project")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def format_path(path: str) -> str:
|
|
21
|
+
"""Format a path for display, using ~ for home directory."""
|
|
22
|
+
home = str(Path.home())
|
|
23
|
+
if path.startswith(home):
|
|
24
|
+
return path.replace(home, "~", 1)
|
|
25
|
+
return path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@project_app.command("list")
|
|
29
|
+
def list_projects() -> None:
|
|
30
|
+
"""List all configured projects."""
|
|
31
|
+
config_manager = ConfigManager()
|
|
32
|
+
projects = config_manager.projects
|
|
33
|
+
|
|
34
|
+
table = Table(title="Basic Memory Projects")
|
|
35
|
+
table.add_column("Name", style="cyan")
|
|
36
|
+
table.add_column("Path", style="green")
|
|
37
|
+
table.add_column("Default", style="yellow")
|
|
38
|
+
table.add_column("Active", style="magenta")
|
|
39
|
+
|
|
40
|
+
default_project = config_manager.default_project
|
|
41
|
+
active_project = config.project
|
|
42
|
+
|
|
43
|
+
for name, path in projects.items():
|
|
44
|
+
is_default = "✓" if name == default_project else ""
|
|
45
|
+
is_active = "✓" if name == active_project else ""
|
|
46
|
+
table.add_row(name, format_path(path), is_default, is_active)
|
|
47
|
+
|
|
48
|
+
console.print(table)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@project_app.command("add")
|
|
52
|
+
def add_project(
|
|
53
|
+
name: str = typer.Argument(..., help="Name of the project"),
|
|
54
|
+
path: str = typer.Argument(..., help="Path to the project directory"),
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Add a new project."""
|
|
57
|
+
config_manager = ConfigManager()
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# Resolve to absolute path
|
|
61
|
+
resolved_path = os.path.abspath(os.path.expanduser(path))
|
|
62
|
+
config_manager.add_project(name, resolved_path)
|
|
63
|
+
console.print(f"[green]Project '{name}' added at {format_path(resolved_path)}[/green]")
|
|
64
|
+
|
|
65
|
+
# Display usage hint
|
|
66
|
+
console.print("\nTo use this project:")
|
|
67
|
+
console.print(f" basic-memory --project={name} <command>")
|
|
68
|
+
console.print(" # or")
|
|
69
|
+
console.print(f" basic-memory project default {name}")
|
|
70
|
+
except ValueError as e:
|
|
71
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
72
|
+
raise typer.Exit(1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@project_app.command("remove")
|
|
76
|
+
def remove_project(
|
|
77
|
+
name: str = typer.Argument(..., help="Name of the project to remove"),
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Remove a project from configuration."""
|
|
80
|
+
config_manager = ConfigManager()
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
config_manager.remove_project(name)
|
|
84
|
+
console.print(f"[green]Project '{name}' removed from configuration[/green]")
|
|
85
|
+
console.print("[yellow]Note: The project files have not been deleted from disk.[/yellow]")
|
|
86
|
+
except ValueError as e: # pragma: no cover
|
|
87
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
88
|
+
raise typer.Exit(1)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@project_app.command("default")
|
|
92
|
+
def set_default_project(
|
|
93
|
+
name: str = typer.Argument(..., help="Name of the project to set as default"),
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Set the default project."""
|
|
96
|
+
config_manager = ConfigManager()
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
config_manager.set_default_project(name)
|
|
100
|
+
console.print(f"[green]Project '{name}' set as default[/green]")
|
|
101
|
+
except ValueError as e: # pragma: no cover
|
|
102
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
103
|
+
raise typer.Exit(1)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@project_app.command("current")
|
|
107
|
+
def show_current_project() -> None:
|
|
108
|
+
"""Show the current project."""
|
|
109
|
+
config_manager = ConfigManager()
|
|
110
|
+
current = os.environ.get("BASIC_MEMORY_PROJECT", config_manager.default_project)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
path = config_manager.get_project_path(current)
|
|
114
|
+
console.print(f"Current project: [cyan]{current}[/cyan]")
|
|
115
|
+
console.print(f"Path: [green]{format_path(str(path))}[/green]")
|
|
116
|
+
console.print(f"Database: [blue]{format_path(str(config.database_path))}[/blue]")
|
|
117
|
+
except ValueError: # pragma: no cover
|
|
118
|
+
console.print(f"[yellow]Warning: Project '{current}' not found in configuration[/yellow]")
|
|
119
|
+
console.print(f"Using default project: [cyan]{config_manager.default_project}[/cyan]")
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""CLI command for project info status."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.tree import Tree
|
|
12
|
+
|
|
13
|
+
from basic_memory.cli.app import app
|
|
14
|
+
from basic_memory.mcp.tools.project_info import project_info
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
info_app = typer.Typer()
|
|
18
|
+
app.add_typer(info_app, name="info", help="Get information about your Basic Memory project")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@info_app.command("stats")
|
|
22
|
+
def display_project_info(
|
|
23
|
+
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
24
|
+
):
|
|
25
|
+
"""Display detailed information and statistics about the current project."""
|
|
26
|
+
try:
|
|
27
|
+
# Get project info
|
|
28
|
+
info = asyncio.run(project_info())
|
|
29
|
+
|
|
30
|
+
if json_output:
|
|
31
|
+
# Convert to JSON and print
|
|
32
|
+
print(json.dumps(info.model_dump(), indent=2, default=str))
|
|
33
|
+
else:
|
|
34
|
+
# Create rich display
|
|
35
|
+
console = Console()
|
|
36
|
+
|
|
37
|
+
# Project configuration section
|
|
38
|
+
console.print(
|
|
39
|
+
Panel(
|
|
40
|
+
f"[bold]Project:[/bold] {info.project_name}\n"
|
|
41
|
+
f"[bold]Path:[/bold] {info.project_path}\n"
|
|
42
|
+
f"[bold]Default Project:[/bold] {info.default_project}\n",
|
|
43
|
+
title="📊 Basic Memory Project Info",
|
|
44
|
+
expand=False,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Statistics section
|
|
49
|
+
stats_table = Table(title="📈 Statistics")
|
|
50
|
+
stats_table.add_column("Metric", style="cyan")
|
|
51
|
+
stats_table.add_column("Count", style="green")
|
|
52
|
+
|
|
53
|
+
stats_table.add_row("Entities", str(info.statistics.total_entities))
|
|
54
|
+
stats_table.add_row("Observations", str(info.statistics.total_observations))
|
|
55
|
+
stats_table.add_row("Relations", str(info.statistics.total_relations))
|
|
56
|
+
stats_table.add_row(
|
|
57
|
+
"Unresolved Relations", str(info.statistics.total_unresolved_relations)
|
|
58
|
+
)
|
|
59
|
+
stats_table.add_row("Isolated Entities", str(info.statistics.isolated_entities))
|
|
60
|
+
|
|
61
|
+
console.print(stats_table)
|
|
62
|
+
|
|
63
|
+
# Entity types
|
|
64
|
+
if info.statistics.entity_types:
|
|
65
|
+
entity_types_table = Table(title="📑 Entity Types")
|
|
66
|
+
entity_types_table.add_column("Type", style="blue")
|
|
67
|
+
entity_types_table.add_column("Count", style="green")
|
|
68
|
+
|
|
69
|
+
for entity_type, count in info.statistics.entity_types.items():
|
|
70
|
+
entity_types_table.add_row(entity_type, str(count))
|
|
71
|
+
|
|
72
|
+
console.print(entity_types_table)
|
|
73
|
+
|
|
74
|
+
# Most connected entities
|
|
75
|
+
if info.statistics.most_connected_entities:
|
|
76
|
+
connected_table = Table(title="🔗 Most Connected Entities")
|
|
77
|
+
connected_table.add_column("Title", style="blue")
|
|
78
|
+
connected_table.add_column("Permalink", style="cyan")
|
|
79
|
+
connected_table.add_column("Relations", style="green")
|
|
80
|
+
|
|
81
|
+
for entity in info.statistics.most_connected_entities:
|
|
82
|
+
connected_table.add_row(
|
|
83
|
+
entity["title"], entity["permalink"], str(entity["relation_count"])
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
console.print(connected_table)
|
|
87
|
+
|
|
88
|
+
# Recent activity
|
|
89
|
+
if info.activity.recently_updated:
|
|
90
|
+
recent_table = Table(title="🕒 Recent Activity")
|
|
91
|
+
recent_table.add_column("Title", style="blue")
|
|
92
|
+
recent_table.add_column("Type", style="cyan")
|
|
93
|
+
recent_table.add_column("Last Updated", style="green")
|
|
94
|
+
|
|
95
|
+
for entity in info.activity.recently_updated[:5]: # Show top 5
|
|
96
|
+
updated_at = (
|
|
97
|
+
datetime.fromisoformat(entity["updated_at"])
|
|
98
|
+
if isinstance(entity["updated_at"], str)
|
|
99
|
+
else entity["updated_at"]
|
|
100
|
+
)
|
|
101
|
+
recent_table.add_row(
|
|
102
|
+
entity["title"],
|
|
103
|
+
entity["entity_type"],
|
|
104
|
+
updated_at.strftime("%Y-%m-%d %H:%M"),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
console.print(recent_table)
|
|
108
|
+
|
|
109
|
+
# System status
|
|
110
|
+
system_tree = Tree("🖥️ System Status")
|
|
111
|
+
system_tree.add(f"Basic Memory version: [bold green]{info.system.version}[/bold green]")
|
|
112
|
+
system_tree.add(
|
|
113
|
+
f"Database: [cyan]{info.system.database_path}[/cyan] ([green]{info.system.database_size}[/green])"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Watch status
|
|
117
|
+
if info.system.watch_status: # pragma: no cover
|
|
118
|
+
watch_branch = system_tree.add("Watch Service")
|
|
119
|
+
running = info.system.watch_status.get("running", False)
|
|
120
|
+
status_color = "green" if running else "red"
|
|
121
|
+
watch_branch.add(
|
|
122
|
+
f"Status: [bold {status_color}]{'Running' if running else 'Stopped'}[/bold {status_color}]"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if running:
|
|
126
|
+
start_time = (
|
|
127
|
+
datetime.fromisoformat(info.system.watch_status.get("start_time", ""))
|
|
128
|
+
if isinstance(info.system.watch_status.get("start_time"), str)
|
|
129
|
+
else info.system.watch_status.get("start_time")
|
|
130
|
+
)
|
|
131
|
+
watch_branch.add(
|
|
132
|
+
f"Running since: [cyan]{start_time.strftime('%Y-%m-%d %H:%M')}[/cyan]"
|
|
133
|
+
)
|
|
134
|
+
watch_branch.add(
|
|
135
|
+
f"Files synced: [green]{info.system.watch_status.get('synced_files', 0)}[/green]"
|
|
136
|
+
)
|
|
137
|
+
watch_branch.add(
|
|
138
|
+
f"Errors: [{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]{info.system.watch_status.get('error_count', 0)}[/{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]"
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
system_tree.add("[yellow]Watch service not running[/yellow]")
|
|
142
|
+
|
|
143
|
+
console.print(system_tree)
|
|
144
|
+
|
|
145
|
+
# Available projects
|
|
146
|
+
projects_table = Table(title="📁 Available Projects")
|
|
147
|
+
projects_table.add_column("Name", style="blue")
|
|
148
|
+
projects_table.add_column("Path", style="cyan")
|
|
149
|
+
projects_table.add_column("Default", style="green")
|
|
150
|
+
|
|
151
|
+
for name, path in info.available_projects.items():
|
|
152
|
+
is_default = name == info.default_project
|
|
153
|
+
projects_table.add_row(name, path, "✓" if is_default else "")
|
|
154
|
+
|
|
155
|
+
console.print(projects_table)
|
|
156
|
+
|
|
157
|
+
# Timestamp
|
|
158
|
+
current_time = (
|
|
159
|
+
datetime.fromisoformat(str(info.system.timestamp))
|
|
160
|
+
if isinstance(info.system.timestamp, str)
|
|
161
|
+
else info.system.timestamp
|
|
162
|
+
)
|
|
163
|
+
console.print(f"\nTimestamp: [cyan]{current_time.strftime('%Y-%m-%d %H:%M:%S')}[/cyan]")
|
|
164
|
+
|
|
165
|
+
except Exception as e: # pragma: no cover
|
|
166
|
+
typer.echo(f"Error getting project info: {e}", err=True)
|
|
167
|
+
raise typer.Exit(1)
|
|
@@ -3,36 +3,22 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from typing import Set, Dict
|
|
5
5
|
|
|
6
|
-
import logfire
|
|
7
6
|
import typer
|
|
8
7
|
from loguru import logger
|
|
9
8
|
from rich.console import Console
|
|
10
9
|
from rich.panel import Panel
|
|
11
10
|
from rich.tree import Tree
|
|
12
11
|
|
|
13
|
-
from basic_memory import db
|
|
14
12
|
from basic_memory.cli.app import app
|
|
13
|
+
from basic_memory.cli.commands.sync import get_sync_service
|
|
15
14
|
from basic_memory.config import config
|
|
16
|
-
from basic_memory.
|
|
17
|
-
from basic_memory.
|
|
18
|
-
from basic_memory.sync import FileChangeScanner
|
|
19
|
-
from basic_memory.sync.utils import SyncReport
|
|
15
|
+
from basic_memory.sync import SyncService
|
|
16
|
+
from basic_memory.sync.sync_service import SyncReport
|
|
20
17
|
|
|
21
18
|
# Create rich console
|
|
22
19
|
console = Console()
|
|
23
20
|
|
|
24
21
|
|
|
25
|
-
async def get_file_change_scanner(
|
|
26
|
-
db_type=DatabaseType.FILESYSTEM,
|
|
27
|
-
) -> FileChangeScanner: # pragma: no cover
|
|
28
|
-
"""Get sync service instance."""
|
|
29
|
-
_, session_maker = await db.get_or_create_db(db_path=config.database_path, db_type=db_type)
|
|
30
|
-
|
|
31
|
-
entity_repository = EntityRepository(session_maker)
|
|
32
|
-
file_change_scanner = FileChangeScanner(entity_repository)
|
|
33
|
-
return file_change_scanner
|
|
34
|
-
|
|
35
|
-
|
|
36
22
|
def add_files_to_tree(
|
|
37
23
|
tree: Tree, paths: Set[str], style: str, checksums: Dict[str, str] | None = None
|
|
38
24
|
):
|
|
@@ -104,7 +90,7 @@ def display_changes(title: str, changes: SyncReport, verbose: bool = False):
|
|
|
104
90
|
"""Display changes using Rich for better visualization."""
|
|
105
91
|
tree = Tree(title)
|
|
106
92
|
|
|
107
|
-
if changes.
|
|
93
|
+
if changes.total == 0:
|
|
108
94
|
tree.add("No changes")
|
|
109
95
|
console.print(Panel(tree, expand=False))
|
|
110
96
|
return
|
|
@@ -135,11 +121,11 @@ def display_changes(title: str, changes: SyncReport, verbose: bool = False):
|
|
|
135
121
|
console.print(Panel(tree, expand=False))
|
|
136
122
|
|
|
137
123
|
|
|
138
|
-
async def run_status(sync_service:
|
|
124
|
+
async def run_status(sync_service: SyncService, verbose: bool = False):
|
|
139
125
|
"""Check sync status of files vs database."""
|
|
140
126
|
# Check knowledge/ directory
|
|
141
|
-
knowledge_changes = await sync_service.
|
|
142
|
-
display_changes("
|
|
127
|
+
knowledge_changes = await sync_service.scan(config.home)
|
|
128
|
+
display_changes("Status", knowledge_changes, verbose)
|
|
143
129
|
|
|
144
130
|
|
|
145
131
|
@app.command()
|
|
@@ -147,10 +133,10 @@ def status(
|
|
|
147
133
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed file information"),
|
|
148
134
|
):
|
|
149
135
|
"""Show sync status between files and database."""
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
136
|
+
try:
|
|
137
|
+
sync_service = asyncio.run(get_sync_service())
|
|
138
|
+
asyncio.run(run_status(sync_service, verbose)) # pragma: no cover
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.exception(f"Error checking status: {e}")
|
|
141
|
+
typer.echo(f"Error checking status: {e}", err=True)
|
|
142
|
+
raise typer.Exit(code=1) # pragma: no cover
|