|
1 | 1 | """Command module for basic-memory project management.""" |
2 | 2 |
|
| 3 | +import asyncio |
3 | 4 | import os |
4 | 5 | from pathlib import Path |
5 | 6 |
|
|
9 | 10 |
|
10 | 11 | from basic_memory.cli.app import app |
11 | 12 | from basic_memory.config import ConfigManager, config |
| 13 | +from basic_memory.mcp.tools.project_info import project_info |
| 14 | +import json |
| 15 | +from datetime import datetime |
| 16 | + |
| 17 | +from rich.panel import Panel |
| 18 | +from rich.tree import Tree |
12 | 19 |
|
13 | 20 | console = Console() |
14 | 21 |
|
@@ -127,3 +134,152 @@ def show_current_project() -> None: |
127 | 134 | except ValueError: # pragma: no cover |
128 | 135 | console.print(f"[yellow]Warning: Project '{current}' not found in configuration[/yellow]") |
129 | 136 | console.print(f"Using default project: [cyan]{config_manager.default_project}[/cyan]") |
| 137 | + |
| 138 | + |
| 139 | +@project_app.command("info") |
| 140 | +def display_project_info( |
| 141 | + json_output: bool = typer.Option(False, "--json", help="Output in JSON format"), |
| 142 | +): |
| 143 | + """Display detailed information and statistics about the current project.""" |
| 144 | + try: |
| 145 | + # Get project info |
| 146 | + info = asyncio.run(project_info()) |
| 147 | + |
| 148 | + if json_output: |
| 149 | + # Convert to JSON and print |
| 150 | + print(json.dumps(info.model_dump(), indent=2, default=str)) |
| 151 | + else: |
| 152 | + # Create rich display |
| 153 | + console = Console() |
| 154 | + |
| 155 | + # Project configuration section |
| 156 | + console.print( |
| 157 | + Panel( |
| 158 | + f"[bold]Project:[/bold] {info.project_name}\n" |
| 159 | + f"[bold]Path:[/bold] {info.project_path}\n" |
| 160 | + f"[bold]Default Project:[/bold] {info.default_project}\n", |
| 161 | + title="📊 Basic Memory Project Info", |
| 162 | + expand=False, |
| 163 | + ) |
| 164 | + ) |
| 165 | + |
| 166 | + # Statistics section |
| 167 | + stats_table = Table(title="📈 Statistics") |
| 168 | + stats_table.add_column("Metric", style="cyan") |
| 169 | + stats_table.add_column("Count", style="green") |
| 170 | + |
| 171 | + stats_table.add_row("Entities", str(info.statistics.total_entities)) |
| 172 | + stats_table.add_row("Observations", str(info.statistics.total_observations)) |
| 173 | + stats_table.add_row("Relations", str(info.statistics.total_relations)) |
| 174 | + stats_table.add_row( |
| 175 | + "Unresolved Relations", str(info.statistics.total_unresolved_relations) |
| 176 | + ) |
| 177 | + stats_table.add_row("Isolated Entities", str(info.statistics.isolated_entities)) |
| 178 | + |
| 179 | + console.print(stats_table) |
| 180 | + |
| 181 | + # Entity types |
| 182 | + if info.statistics.entity_types: |
| 183 | + entity_types_table = Table(title="📑 Entity Types") |
| 184 | + entity_types_table.add_column("Type", style="blue") |
| 185 | + entity_types_table.add_column("Count", style="green") |
| 186 | + |
| 187 | + for entity_type, count in info.statistics.entity_types.items(): |
| 188 | + entity_types_table.add_row(entity_type, str(count)) |
| 189 | + |
| 190 | + console.print(entity_types_table) |
| 191 | + |
| 192 | + # Most connected entities |
| 193 | + if info.statistics.most_connected_entities: |
| 194 | + connected_table = Table(title="🔗 Most Connected Entities") |
| 195 | + connected_table.add_column("Title", style="blue") |
| 196 | + connected_table.add_column("Permalink", style="cyan") |
| 197 | + connected_table.add_column("Relations", style="green") |
| 198 | + |
| 199 | + for entity in info.statistics.most_connected_entities: |
| 200 | + connected_table.add_row( |
| 201 | + entity["title"], entity["permalink"], str(entity["relation_count"]) |
| 202 | + ) |
| 203 | + |
| 204 | + console.print(connected_table) |
| 205 | + |
| 206 | + # Recent activity |
| 207 | + if info.activity.recently_updated: |
| 208 | + recent_table = Table(title="🕒 Recent Activity") |
| 209 | + recent_table.add_column("Title", style="blue") |
| 210 | + recent_table.add_column("Type", style="cyan") |
| 211 | + recent_table.add_column("Last Updated", style="green") |
| 212 | + |
| 213 | + for entity in info.activity.recently_updated[:5]: # Show top 5 |
| 214 | + updated_at = ( |
| 215 | + datetime.fromisoformat(entity["updated_at"]) |
| 216 | + if isinstance(entity["updated_at"], str) |
| 217 | + else entity["updated_at"] |
| 218 | + ) |
| 219 | + recent_table.add_row( |
| 220 | + entity["title"], |
| 221 | + entity["entity_type"], |
| 222 | + updated_at.strftime("%Y-%m-%d %H:%M"), |
| 223 | + ) |
| 224 | + |
| 225 | + console.print(recent_table) |
| 226 | + |
| 227 | + # System status |
| 228 | + system_tree = Tree("🖥️ System Status") |
| 229 | + system_tree.add(f"Basic Memory version: [bold green]{info.system.version}[/bold green]") |
| 230 | + system_tree.add( |
| 231 | + f"Database: [cyan]{info.system.database_path}[/cyan] ([green]{info.system.database_size}[/green])" |
| 232 | + ) |
| 233 | + |
| 234 | + # Watch status |
| 235 | + if info.system.watch_status: # pragma: no cover |
| 236 | + watch_branch = system_tree.add("Watch Service") |
| 237 | + running = info.system.watch_status.get("running", False) |
| 238 | + status_color = "green" if running else "red" |
| 239 | + watch_branch.add( |
| 240 | + f"Status: [bold {status_color}]{'Running' if running else 'Stopped'}[/bold {status_color}]" |
| 241 | + ) |
| 242 | + |
| 243 | + if running: |
| 244 | + start_time = ( |
| 245 | + datetime.fromisoformat(info.system.watch_status.get("start_time", "")) |
| 246 | + if isinstance(info.system.watch_status.get("start_time"), str) |
| 247 | + else info.system.watch_status.get("start_time") |
| 248 | + ) |
| 249 | + watch_branch.add( |
| 250 | + f"Running since: [cyan]{start_time.strftime('%Y-%m-%d %H:%M')}[/cyan]" |
| 251 | + ) |
| 252 | + watch_branch.add( |
| 253 | + f"Files synced: [green]{info.system.watch_status.get('synced_files', 0)}[/green]" |
| 254 | + ) |
| 255 | + watch_branch.add( |
| 256 | + 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'}]" |
| 257 | + ) |
| 258 | + else: |
| 259 | + system_tree.add("[yellow]Watch service not running[/yellow]") |
| 260 | + |
| 261 | + console.print(system_tree) |
| 262 | + |
| 263 | + # Available projects |
| 264 | + projects_table = Table(title="📁 Available Projects") |
| 265 | + projects_table.add_column("Name", style="blue") |
| 266 | + projects_table.add_column("Path", style="cyan") |
| 267 | + projects_table.add_column("Default", style="green") |
| 268 | + |
| 269 | + for name, path in info.available_projects.items(): |
| 270 | + is_default = name == info.default_project |
| 271 | + projects_table.add_row(name, path, "✓" if is_default else "") |
| 272 | + |
| 273 | + console.print(projects_table) |
| 274 | + |
| 275 | + # Timestamp |
| 276 | + current_time = ( |
| 277 | + datetime.fromisoformat(str(info.system.timestamp)) |
| 278 | + if isinstance(info.system.timestamp, str) |
| 279 | + else info.system.timestamp |
| 280 | + ) |
| 281 | + console.print(f"\nTimestamp: [cyan]{current_time.strftime('%Y-%m-%d %H:%M:%S')}[/cyan]") |
| 282 | + |
| 283 | + except Exception as e: # pragma: no cover |
| 284 | + typer.echo(f"Error getting project info: {e}", err=True) |
| 285 | + raise typer.Exit(1) |
0 commit comments