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.

Files changed (89) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +23 -1
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  6. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  7. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +106 -0
  8. basic_memory/api/app.py +9 -10
  9. basic_memory/api/routers/__init__.py +2 -1
  10. basic_memory/api/routers/knowledge_router.py +31 -5
  11. basic_memory/api/routers/memory_router.py +18 -17
  12. basic_memory/api/routers/project_info_router.py +275 -0
  13. basic_memory/api/routers/resource_router.py +105 -4
  14. basic_memory/api/routers/search_router.py +22 -4
  15. basic_memory/cli/app.py +54 -5
  16. basic_memory/cli/commands/__init__.py +15 -2
  17. basic_memory/cli/commands/db.py +9 -13
  18. basic_memory/cli/commands/import_chatgpt.py +26 -30
  19. basic_memory/cli/commands/import_claude_conversations.py +27 -29
  20. basic_memory/cli/commands/import_claude_projects.py +29 -31
  21. basic_memory/cli/commands/import_memory_json.py +26 -28
  22. basic_memory/cli/commands/mcp.py +7 -1
  23. basic_memory/cli/commands/project.py +119 -0
  24. basic_memory/cli/commands/project_info.py +167 -0
  25. basic_memory/cli/commands/status.py +14 -28
  26. basic_memory/cli/commands/sync.py +63 -22
  27. basic_memory/cli/commands/tool.py +253 -0
  28. basic_memory/cli/main.py +39 -1
  29. basic_memory/config.py +166 -4
  30. basic_memory/db.py +19 -4
  31. basic_memory/deps.py +10 -3
  32. basic_memory/file_utils.py +37 -19
  33. basic_memory/markdown/entity_parser.py +3 -3
  34. basic_memory/markdown/utils.py +5 -0
  35. basic_memory/mcp/async_client.py +1 -1
  36. basic_memory/mcp/main.py +24 -0
  37. basic_memory/mcp/prompts/__init__.py +19 -0
  38. basic_memory/mcp/prompts/ai_assistant_guide.py +26 -0
  39. basic_memory/mcp/prompts/continue_conversation.py +111 -0
  40. basic_memory/mcp/prompts/recent_activity.py +88 -0
  41. basic_memory/mcp/prompts/search.py +182 -0
  42. basic_memory/mcp/prompts/utils.py +155 -0
  43. basic_memory/mcp/server.py +2 -6
  44. basic_memory/mcp/tools/__init__.py +12 -21
  45. basic_memory/mcp/tools/build_context.py +85 -0
  46. basic_memory/mcp/tools/canvas.py +97 -0
  47. basic_memory/mcp/tools/delete_note.py +28 -0
  48. basic_memory/mcp/tools/project_info.py +51 -0
  49. basic_memory/mcp/tools/read_content.py +229 -0
  50. basic_memory/mcp/tools/read_note.py +190 -0
  51. basic_memory/mcp/tools/recent_activity.py +100 -0
  52. basic_memory/mcp/tools/search.py +56 -17
  53. basic_memory/mcp/tools/utils.py +245 -16
  54. basic_memory/mcp/tools/write_note.py +124 -0
  55. basic_memory/models/knowledge.py +27 -11
  56. basic_memory/models/search.py +2 -1
  57. basic_memory/repository/entity_repository.py +3 -2
  58. basic_memory/repository/project_info_repository.py +9 -0
  59. basic_memory/repository/repository.py +24 -7
  60. basic_memory/repository/search_repository.py +47 -14
  61. basic_memory/schemas/__init__.py +10 -9
  62. basic_memory/schemas/base.py +4 -1
  63. basic_memory/schemas/memory.py +14 -4
  64. basic_memory/schemas/project_info.py +96 -0
  65. basic_memory/schemas/search.py +29 -33
  66. basic_memory/services/context_service.py +3 -3
  67. basic_memory/services/entity_service.py +26 -13
  68. basic_memory/services/file_service.py +145 -26
  69. basic_memory/services/link_resolver.py +9 -46
  70. basic_memory/services/search_service.py +95 -22
  71. basic_memory/sync/__init__.py +3 -2
  72. basic_memory/sync/sync_service.py +523 -117
  73. basic_memory/sync/watch_service.py +258 -132
  74. basic_memory/utils.py +51 -36
  75. basic_memory-0.9.0.dist-info/METADATA +736 -0
  76. basic_memory-0.9.0.dist-info/RECORD +99 -0
  77. basic_memory/alembic/README +0 -1
  78. basic_memory/cli/commands/tools.py +0 -157
  79. basic_memory/mcp/tools/knowledge.py +0 -68
  80. basic_memory/mcp/tools/memory.py +0 -170
  81. basic_memory/mcp/tools/notes.py +0 -202
  82. basic_memory/schemas/discovery.py +0 -28
  83. basic_memory/sync/file_change_scanner.py +0 -158
  84. basic_memory/sync/utils.py +0 -31
  85. basic_memory-0.7.0.dist-info/METADATA +0 -378
  86. basic_memory-0.7.0.dist-info/RECORD +0 -82
  87. {basic_memory-0.7.0.dist-info → basic_memory-0.9.0.dist-info}/WHEEL +0 -0
  88. {basic_memory-0.7.0.dist-info → basic_memory-0.9.0.dist-info}/entry_points.txt +0 -0
  89. {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
- 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)
@@ -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.db import DatabaseType
17
- from basic_memory.repository import EntityRepository
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.total_changes == 0:
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: FileChangeScanner, verbose: bool = False):
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.find_knowledge_changes(config.home)
142
- display_changes("Knowledge Files", knowledge_changes, verbose)
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
- with logfire.span("status"): # pyright: ignore [reportGeneralTypeIssues]
151
- try:
152
- sync_service = asyncio.run(get_file_change_scanner())
153
- asyncio.run(run_status(sync_service, verbose)) # pragma: no cover
154
- except Exception as e:
155
- logger.exception(f"Error checking status: {e}")
156
- raise typer.Exit(code=1) # pragma: no cover
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