basic-memory 0.8.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.

Files changed (75) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/migrations.py +4 -9
  3. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +106 -0
  4. basic_memory/api/app.py +9 -6
  5. basic_memory/api/routers/__init__.py +2 -1
  6. basic_memory/api/routers/knowledge_router.py +30 -4
  7. basic_memory/api/routers/memory_router.py +3 -2
  8. basic_memory/api/routers/project_info_router.py +275 -0
  9. basic_memory/api/routers/search_router.py +22 -4
  10. basic_memory/cli/app.py +54 -3
  11. basic_memory/cli/commands/__init__.py +15 -2
  12. basic_memory/cli/commands/db.py +9 -13
  13. basic_memory/cli/commands/import_chatgpt.py +26 -30
  14. basic_memory/cli/commands/import_claude_conversations.py +27 -29
  15. basic_memory/cli/commands/import_claude_projects.py +29 -31
  16. basic_memory/cli/commands/import_memory_json.py +26 -28
  17. basic_memory/cli/commands/mcp.py +7 -1
  18. basic_memory/cli/commands/project.py +119 -0
  19. basic_memory/cli/commands/project_info.py +167 -0
  20. basic_memory/cli/commands/status.py +7 -9
  21. basic_memory/cli/commands/sync.py +54 -9
  22. basic_memory/cli/commands/{tools.py → tool.py} +92 -19
  23. basic_memory/cli/main.py +40 -1
  24. basic_memory/config.py +155 -7
  25. basic_memory/db.py +19 -4
  26. basic_memory/deps.py +10 -3
  27. basic_memory/file_utils.py +32 -16
  28. basic_memory/markdown/utils.py +5 -0
  29. basic_memory/mcp/main.py +1 -2
  30. basic_memory/mcp/prompts/__init__.py +6 -2
  31. basic_memory/mcp/prompts/ai_assistant_guide.py +6 -8
  32. basic_memory/mcp/prompts/continue_conversation.py +65 -126
  33. basic_memory/mcp/prompts/recent_activity.py +55 -13
  34. basic_memory/mcp/prompts/search.py +72 -17
  35. basic_memory/mcp/prompts/utils.py +139 -82
  36. basic_memory/mcp/server.py +1 -1
  37. basic_memory/mcp/tools/__init__.py +11 -22
  38. basic_memory/mcp/tools/build_context.py +85 -0
  39. basic_memory/mcp/tools/canvas.py +17 -19
  40. basic_memory/mcp/tools/delete_note.py +28 -0
  41. basic_memory/mcp/tools/project_info.py +51 -0
  42. basic_memory/mcp/tools/{resource.py → read_content.py} +42 -5
  43. basic_memory/mcp/tools/read_note.py +190 -0
  44. basic_memory/mcp/tools/recent_activity.py +100 -0
  45. basic_memory/mcp/tools/search.py +56 -17
  46. basic_memory/mcp/tools/utils.py +245 -17
  47. basic_memory/mcp/tools/write_note.py +124 -0
  48. basic_memory/models/search.py +2 -1
  49. basic_memory/repository/entity_repository.py +3 -2
  50. basic_memory/repository/project_info_repository.py +9 -0
  51. basic_memory/repository/repository.py +23 -6
  52. basic_memory/repository/search_repository.py +33 -10
  53. basic_memory/schemas/__init__.py +12 -0
  54. basic_memory/schemas/memory.py +3 -2
  55. basic_memory/schemas/project_info.py +96 -0
  56. basic_memory/schemas/search.py +27 -32
  57. basic_memory/services/context_service.py +3 -3
  58. basic_memory/services/entity_service.py +8 -2
  59. basic_memory/services/file_service.py +105 -53
  60. basic_memory/services/link_resolver.py +5 -45
  61. basic_memory/services/search_service.py +45 -16
  62. basic_memory/sync/sync_service.py +274 -39
  63. basic_memory/sync/watch_service.py +160 -30
  64. basic_memory/utils.py +40 -40
  65. basic_memory-0.9.0.dist-info/METADATA +736 -0
  66. basic_memory-0.9.0.dist-info/RECORD +99 -0
  67. basic_memory/mcp/prompts/json_canvas_spec.py +0 -25
  68. basic_memory/mcp/tools/knowledge.py +0 -68
  69. basic_memory/mcp/tools/memory.py +0 -177
  70. basic_memory/mcp/tools/notes.py +0 -201
  71. basic_memory-0.8.0.dist-info/METADATA +0 -379
  72. basic_memory-0.8.0.dist-info/RECORD +0 -91
  73. {basic_memory-0.8.0.dist-info → basic_memory-0.9.0.dist-info}/WHEEL +0 -0
  74. {basic_memory-0.8.0.dist-info → basic_memory-0.9.0.dist-info}/entry_points.txt +0 -0
  75. {basic_memory-0.8.0.dist-info → basic_memory-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,18 @@
1
1
  """CLI commands for basic-memory."""
2
2
 
3
- from . import status, sync, db, import_memory_json, mcp
3
+ 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
5
 
5
- __all__ = ["status", "sync", "db", "import_memory_json", "mcp"]
6
+ __all__ = [
7
+ "status",
8
+ "sync",
9
+ "db",
10
+ "import_memory_json",
11
+ "mcp",
12
+ "import_claude_conversations",
13
+ "import_claude_projects",
14
+ "import_chatgpt",
15
+ "tool",
16
+ "project",
17
+ "project_info",
18
+ ]
@@ -1,8 +1,5 @@
1
1
  """Database management commands."""
2
2
 
3
- import asyncio
4
-
5
- import logfire
6
3
  import typer
7
4
  from loguru import logger
8
5
 
@@ -12,17 +9,16 @@ from basic_memory.cli.app import app
12
9
 
13
10
  @app.command()
14
11
  def reset(
15
- reindex: bool = typer.Option(False, "--reindex", help="Rebuild indices from filesystem"),
12
+ reindex: bool = typer.Option(False, "--reindex", help="Rebuild db index from filesystem"),
16
13
  ): # pragma: no cover
17
14
  """Reset database (drop all tables and recreate)."""
18
- with logfire.span("reset"): # pyright: ignore [reportGeneralTypeIssues]
19
- if typer.confirm("This will delete all data in your db. Are you sure?"):
20
- logger.info("Resetting database...")
21
- asyncio.run(migrations.reset_database())
15
+ if typer.confirm("This will delete all data in your db. Are you sure?"):
16
+ logger.info("Resetting database...")
17
+ migrations.reset_database()
22
18
 
23
- if reindex:
24
- # Import and run sync
25
- from basic_memory.cli.commands.sync import sync
19
+ if reindex:
20
+ # Import and run sync
21
+ from basic_memory.cli.commands.sync import sync
26
22
 
27
- logger.info("Rebuilding search index from filesystem...")
28
- sync(watch=False) # pyright: ignore
23
+ logger.info("Rebuilding search index from filesystem...")
24
+ sync(watch=False) # pyright: ignore
@@ -6,7 +6,6 @@ from datetime import datetime
6
6
  from pathlib import Path
7
7
  from typing import Dict, Any, List, Annotated, Set, Optional
8
8
 
9
- import logfire
10
9
  import typer
11
10
  from loguru import logger
12
11
  from rich.console import Console
@@ -226,38 +225,35 @@ def import_chatgpt(
226
225
  After importing, run 'basic-memory sync' to index the new files.
227
226
  """
228
227
 
229
- with logfire.span("import chatgpt"): # pyright: ignore [reportGeneralTypeIssues]
230
- try:
231
- if conversations_json:
232
- if not conversations_json.exists():
233
- typer.echo(f"Error: File not found: {conversations_json}", err=True)
234
- raise typer.Exit(1)
228
+ try:
229
+ if conversations_json:
230
+ if not conversations_json.exists():
231
+ typer.echo(f"Error: File not found: {conversations_json}", err=True)
232
+ raise typer.Exit(1)
235
233
 
236
- # Get markdown processor
237
- markdown_processor = asyncio.run(get_markdown_processor())
234
+ # Get markdown processor
235
+ markdown_processor = asyncio.run(get_markdown_processor())
238
236
 
239
- # Process the file
240
- base_path = config.home / folder
241
- console.print(
242
- f"\nImporting chats from {conversations_json}...writing to {base_path}"
243
- )
244
- results = asyncio.run(
245
- process_chatgpt_json(conversations_json, folder, markdown_processor)
246
- )
237
+ # Process the file
238
+ base_path = config.home / folder
239
+ console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
240
+ results = asyncio.run(
241
+ process_chatgpt_json(conversations_json, folder, markdown_processor)
242
+ )
247
243
 
248
- # Show results
249
- console.print(
250
- Panel(
251
- f"[green]Import complete![/green]\n\n"
252
- f"Imported {results['conversations']} conversations\n"
253
- f"Containing {results['messages']} messages",
254
- expand=False,
255
- )
244
+ # Show results
245
+ console.print(
246
+ Panel(
247
+ f"[green]Import complete![/green]\n\n"
248
+ f"Imported {results['conversations']} conversations\n"
249
+ f"Containing {results['messages']} messages",
250
+ expand=False,
256
251
  )
252
+ )
257
253
 
258
- console.print("\nRun 'basic-memory sync' to index the new files.")
254
+ console.print("\nRun 'basic-memory sync' to index the new files.")
259
255
 
260
- except Exception as e:
261
- logger.error("Import failed")
262
- typer.echo(f"Error during import: {e}", err=True)
263
- raise typer.Exit(1)
256
+ except Exception as e:
257
+ logger.error("Import failed")
258
+ typer.echo(f"Error during import: {e}", err=True)
259
+ raise typer.Exit(1)
@@ -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
- with logfire.span("import claude conversations"): # pyright: ignore [reportGeneralTypeIssues]
183
- try:
184
- if not conversations_json.exists():
185
- typer.echo(f"Error: File not found: {conversations_json}", err=True)
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
- # Show results
199
- console.print(
200
- Panel(
201
- f"[green]Import complete![/green]\n\n"
202
- f"Imported {results['conversations']} conversations\n"
203
- f"Containing {results['messages']} messages",
204
- expand=False,
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
- console.print("\nRun 'basic-memory sync' to index the new files.")
206
+ console.print("\nRun 'basic-memory sync' to index the new files.")
209
207
 
210
- except Exception as e:
211
- logger.error("Import failed")
212
- typer.echo(f"Error during import: {e}", err=True)
213
- raise typer.Exit(1)
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
- with logfire.span("import claude projects"): # pyright: ignore [reportGeneralTypeIssues]
165
- try:
166
- if projects_json:
167
- if not projects_json.exists():
168
- typer.echo(f"Error: File not found: {projects_json}", err=True)
169
- raise typer.Exit(1)
170
-
171
- # Get markdown processor
172
- markdown_processor = asyncio.run(get_markdown_processor())
173
-
174
- # Process the file
175
- base_path = config.home / base_folder if base_folder else config.home
176
- console.print(f"\nImporting projects from {projects_json}...writing to {base_path}")
177
- results = asyncio.run(
178
- process_projects_json(projects_json, base_path, markdown_processor)
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
- # Show results
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
- except Exception as e:
194
- logger.error("Import failed")
195
- typer.echo(f"Error during import: {e}", err=True)
196
- raise typer.Exit(1)
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
- with logfire.span("import memory_json"): # pyright: ignore [reportGeneralTypeIssues]
118
- if not json_path.exists():
119
- typer.echo(f"Error: File not found: {json_path}", err=True)
120
- raise typer.Exit(1)
121
-
122
- try:
123
- # Get markdown processor
124
- markdown_processor = asyncio.run(get_markdown_processor())
125
-
126
- # Process the file
127
- base_path = config.home
128
- console.print(f"\nImporting from {json_path}...writing to {base_path}")
129
- results = asyncio.run(process_memory_json(json_path, base_path, markdown_processor))
130
-
131
- # Show results
132
- console.print(
133
- Panel(
134
- f"[green]Import complete![/green]\n\n"
135
- f"Created {results['entities']} entities\n"
136
- f"Added {results['relations']} relations",
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
- console.print("\nRun 'basic-memory sync' to index the new files.")
139
+ console.print("\nRun 'basic-memory sync' to index the new files.")
142
140
 
143
- except Exception as e:
144
- logger.error("Import failed")
145
- typer.echo(f"Error during import: {e}", err=True)
146
- raise typer.Exit(1)
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)
@@ -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"Home directory: {home_dir}")
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)