Skip to content

Commit 2a881b1

Browse files
committed
refactor: move project stats into projct subcommand
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 681af5d commit 2a881b1

File tree

4 files changed

+159
-171
lines changed

4 files changed

+159
-171
lines changed

src/basic_memory/cli/commands/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""CLI commands for basic-memory."""
22

33
from . import status, sync, db, import_memory_json, mcp, import_claude_conversations
4-
from . import import_claude_projects, import_chatgpt, tool, project, project_info
4+
from . import import_claude_projects, import_chatgpt, tool, project
55

66
__all__ = [
77
"status",
@@ -14,5 +14,4 @@
1414
"import_chatgpt",
1515
"tool",
1616
"project",
17-
"project_info",
1817
]

src/basic_memory/cli/commands/project.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Command module for basic-memory project management."""
22

3+
import asyncio
34
import os
45
from pathlib import Path
56

@@ -9,6 +10,12 @@
910

1011
from basic_memory.cli.app import app
1112
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
1219

1320
console = Console()
1421

@@ -127,3 +134,152 @@ def show_current_project() -> None:
127134
except ValueError: # pragma: no cover
128135
console.print(f"[yellow]Warning: Project '{current}' not found in configuration[/yellow]")
129136
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)

src/basic_memory/cli/commands/project_info.py

Lines changed: 0 additions & 167 deletions
This file was deleted.

tests/cli/test_project_info.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def test_info_stats_command(cli_env, test_graph):
1212
runner = CliRunner()
1313

1414
# Run the command
15-
result = runner.invoke(cli_app, ["info", "stats"])
15+
result = runner.invoke(cli_app, ["project", "info"])
1616

1717
# Verify exit code
1818
assert result.exit_code == 0
@@ -26,7 +26,7 @@ def test_info_stats_json(cli_env, test_graph):
2626
runner = CliRunner()
2727

2828
# Run the command with --json flag
29-
result = runner.invoke(cli_app, ["info", "stats", "--json"])
29+
result = runner.invoke(cli_app, ["project", "info", "--json"])
3030

3131
# Verify exit code
3232
assert result.exit_code == 0

0 commit comments

Comments
 (0)