basic-memory 0.8.0__py3-none-any.whl → 0.10.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 (76) 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 +274 -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 +31 -36
  14. basic_memory/cli/commands/import_claude_conversations.py +32 -35
  15. basic_memory/cli/commands/import_claude_projects.py +34 -37
  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 +157 -10
  25. basic_memory/db.py +19 -4
  26. basic_memory/deps.py +10 -3
  27. basic_memory/file_utils.py +34 -18
  28. basic_memory/markdown/markdown_processor.py +1 -1
  29. basic_memory/markdown/utils.py +5 -0
  30. basic_memory/mcp/main.py +1 -2
  31. basic_memory/mcp/prompts/__init__.py +6 -2
  32. basic_memory/mcp/prompts/ai_assistant_guide.py +9 -10
  33. basic_memory/mcp/prompts/continue_conversation.py +65 -126
  34. basic_memory/mcp/prompts/recent_activity.py +55 -13
  35. basic_memory/mcp/prompts/search.py +72 -17
  36. basic_memory/mcp/prompts/utils.py +139 -82
  37. basic_memory/mcp/server.py +1 -1
  38. basic_memory/mcp/tools/__init__.py +11 -22
  39. basic_memory/mcp/tools/build_context.py +85 -0
  40. basic_memory/mcp/tools/canvas.py +17 -19
  41. basic_memory/mcp/tools/delete_note.py +28 -0
  42. basic_memory/mcp/tools/project_info.py +51 -0
  43. basic_memory/mcp/tools/{resource.py → read_content.py} +42 -5
  44. basic_memory/mcp/tools/read_note.py +190 -0
  45. basic_memory/mcp/tools/recent_activity.py +100 -0
  46. basic_memory/mcp/tools/search.py +56 -17
  47. basic_memory/mcp/tools/utils.py +245 -17
  48. basic_memory/mcp/tools/write_note.py +124 -0
  49. basic_memory/models/search.py +2 -1
  50. basic_memory/repository/entity_repository.py +3 -2
  51. basic_memory/repository/project_info_repository.py +9 -0
  52. basic_memory/repository/repository.py +23 -6
  53. basic_memory/repository/search_repository.py +33 -10
  54. basic_memory/schemas/__init__.py +12 -0
  55. basic_memory/schemas/memory.py +3 -2
  56. basic_memory/schemas/project_info.py +96 -0
  57. basic_memory/schemas/search.py +27 -32
  58. basic_memory/services/context_service.py +3 -3
  59. basic_memory/services/entity_service.py +8 -2
  60. basic_memory/services/file_service.py +107 -57
  61. basic_memory/services/link_resolver.py +5 -45
  62. basic_memory/services/search_service.py +45 -16
  63. basic_memory/sync/sync_service.py +274 -39
  64. basic_memory/sync/watch_service.py +174 -34
  65. basic_memory/utils.py +40 -40
  66. basic_memory-0.10.0.dist-info/METADATA +386 -0
  67. basic_memory-0.10.0.dist-info/RECORD +99 -0
  68. basic_memory/mcp/prompts/json_canvas_spec.py +0 -25
  69. basic_memory/mcp/tools/knowledge.py +0 -68
  70. basic_memory/mcp/tools/memory.py +0 -177
  71. basic_memory/mcp/tools/notes.py +0 -201
  72. basic_memory-0.8.0.dist-info/METADATA +0 -379
  73. basic_memory-0.8.0.dist-info/RECORD +0 -91
  74. {basic_memory-0.8.0.dist-info → basic_memory-0.10.0.dist-info}/WHEEL +0 -0
  75. {basic_memory-0.8.0.dist-info → basic_memory-0.10.0.dist-info}/entry_points.txt +0 -0
  76. {basic_memory-0.8.0.dist-info → basic_memory-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -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,7 +3,6 @@
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
@@ -134,11 +133,10 @@ def status(
134
133
  verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed file information"),
135
134
  ):
136
135
  """Show sync status between files and database."""
137
- with logfire.span("status"): # pyright: ignore [reportGeneralTypeIssues]
138
- try:
139
- sync_service = asyncio.run(get_sync_service())
140
- asyncio.run(run_status(sync_service, verbose)) # pragma: no cover
141
- except Exception as e:
142
- logger.exception(f"Error checking status: {e}")
143
- typer.echo(f"Error checking status: {e}", err=True)
144
- 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
@@ -93,8 +93,10 @@ def group_issues_by_directory(issues: List[ValidationIssue]) -> Dict[str, List[V
93
93
  def display_sync_summary(knowledge: SyncReport):
94
94
  """Display a one-line summary of sync changes."""
95
95
  total_changes = knowledge.total
96
+ project_name = config.project
97
+
96
98
  if total_changes == 0:
97
- console.print("[green]Everything up to date[/green]")
99
+ console.print(f"[green]Project '{project_name}': Everything up to date[/green]")
98
100
  return
99
101
 
100
102
  # Format as: "Synced X files (A new, B modified, C moved, D deleted)"
@@ -113,16 +115,18 @@ def display_sync_summary(knowledge: SyncReport):
113
115
  if del_count:
114
116
  changes.append(f"[red]{del_count} deleted[/red]")
115
117
 
116
- console.print(f"Synced {total_changes} files ({', '.join(changes)})")
118
+ console.print(f"Project '{project_name}': Synced {total_changes} files ({', '.join(changes)})")
117
119
 
118
120
 
119
121
  def display_detailed_sync_results(knowledge: SyncReport):
120
122
  """Display detailed sync results with trees."""
123
+ project_name = config.project
124
+
121
125
  if knowledge.total == 0:
122
- console.print("\n[green]Everything up to date[/green]")
126
+ console.print(f"\n[green]Project '{project_name}': Everything up to date[/green]")
123
127
  return
124
128
 
125
- console.print("\n[bold]Sync Results[/bold]")
129
+ console.print(f"\n[bold]Sync Results for Project '{project_name}'[/bold]")
126
130
 
127
131
  if knowledge.total > 0:
128
132
  knowledge_tree = Tree("[bold]Knowledge Files[/bold]")
@@ -150,23 +154,52 @@ def display_detailed_sync_results(knowledge: SyncReport):
150
154
 
151
155
  async def run_sync(verbose: bool = False, watch: bool = False, console_status: bool = False):
152
156
  """Run sync operation."""
157
+ import time
158
+
159
+ start_time = time.time()
160
+
161
+ logger.info(
162
+ "Sync command started",
163
+ project=config.project,
164
+ watch_mode=watch,
165
+ verbose=verbose,
166
+ directory=str(config.home),
167
+ )
153
168
 
154
169
  sync_service = await get_sync_service()
155
170
 
156
171
  # Start watching if requested
157
172
  if watch:
173
+ logger.info("Starting watch service after initial sync")
158
174
  watch_service = WatchService(
159
175
  sync_service=sync_service,
160
176
  file_service=sync_service.entity_service.file_service,
161
177
  config=config,
162
178
  )
163
- # full sync
164
- await sync_service.sync(config.home)
179
+
180
+ # full sync - no progress bars in watch mode
181
+ await sync_service.sync(config.home, show_progress=False)
182
+
165
183
  # watch changes
166
184
  await watch_service.run() # pragma: no cover
167
185
  else:
168
- # one time sync
169
- knowledge_changes = await sync_service.sync(config.home)
186
+ # one time sync - use progress bars for better UX
187
+ logger.info("Running one-time sync")
188
+ knowledge_changes = await sync_service.sync(config.home, show_progress=True)
189
+
190
+ # Log results
191
+ duration_ms = int((time.time() - start_time) * 1000)
192
+ logger.info(
193
+ "Sync command completed",
194
+ project=config.project,
195
+ total_changes=knowledge_changes.total,
196
+ new_files=len(knowledge_changes.new),
197
+ modified_files=len(knowledge_changes.modified),
198
+ deleted_files=len(knowledge_changes.deleted),
199
+ moved_files=len(knowledge_changes.moves),
200
+ duration_ms=duration_ms,
201
+ )
202
+
170
203
  # Display results
171
204
  if verbose:
172
205
  display_detailed_sync_results(knowledge_changes)
@@ -191,12 +224,24 @@ def sync(
191
224
  ) -> None:
192
225
  """Sync knowledge files with the database."""
193
226
  try:
227
+ # Show which project we're syncing
228
+ if not watch: # Don't show in watch mode as it would break the UI
229
+ typer.echo(f"Syncing project: {config.project}")
230
+ typer.echo(f"Project path: {config.home}")
231
+
194
232
  # Run sync
195
233
  asyncio.run(run_sync(verbose=verbose, watch=watch))
196
234
 
197
235
  except Exception as e: # pragma: no cover
198
236
  if not isinstance(e, typer.Exit):
199
- logger.exception("Sync failed", e)
237
+ logger.exception(
238
+ "Sync command failed",
239
+ project=config.project,
240
+ error=str(e),
241
+ error_type=type(e).__name__,
242
+ watch_mode=watch,
243
+ directory=str(config.home),
244
+ )
200
245
  typer.echo(f"Error during sync: {e}", err=True)
201
246
  raise typer.Exit(1)
202
247
  raise
@@ -1,6 +1,7 @@
1
1
  """CLI tool commands for Basic Memory."""
2
2
 
3
3
  import asyncio
4
+ import sys
4
5
  from typing import Optional, List, Annotated
5
6
 
6
7
  import typer
@@ -9,7 +10,6 @@ from rich import print as rprint
9
10
 
10
11
  from basic_memory.cli.app import app
11
12
  from basic_memory.mcp.tools import build_context as mcp_build_context
12
- from basic_memory.mcp.tools import get_entity as mcp_get_entity
13
13
  from basic_memory.mcp.tools import read_note as mcp_read_note
14
14
  from basic_memory.mcp.tools import recent_activity as mcp_recent_activity
15
15
  from basic_memory.mcp.tools import search as mcp_search
@@ -20,24 +20,78 @@ from basic_memory.mcp.prompts.continue_conversation import (
20
20
  continue_conversation as mcp_continue_conversation,
21
21
  )
22
22
 
23
+ from basic_memory.mcp.prompts.recent_activity import (
24
+ recent_activity_prompt as recent_activity_prompt,
25
+ )
26
+
23
27
  from basic_memory.schemas.base import TimeFrame
24
28
  from basic_memory.schemas.memory import MemoryUrl
25
29
  from basic_memory.schemas.search import SearchQuery, SearchItemType
26
30
 
27
31
  tool_app = typer.Typer()
28
- app.add_typer(tool_app, name="tools", help="cli versions mcp tools")
32
+ app.add_typer(tool_app, name="tool", help="Direct access to MCP tools via CLI")
29
33
 
30
34
 
31
35
  @tool_app.command()
32
36
  def write_note(
33
37
  title: Annotated[str, typer.Option(help="The title of the note")],
34
- content: Annotated[str, typer.Option(help="The content of the note")],
35
38
  folder: Annotated[str, typer.Option(help="The folder to create the note in")],
39
+ content: Annotated[
40
+ Optional[str],
41
+ typer.Option(
42
+ help="The content of the note. If not provided, content will be read from stdin. This allows piping content from other commands, e.g.: cat file.md | basic-memory tools write-note"
43
+ ),
44
+ ] = None,
36
45
  tags: Annotated[
37
46
  Optional[List[str]], typer.Option(help="A list of tags to apply to the note")
38
47
  ] = None,
39
48
  ):
49
+ """Create or update a markdown note. Content can be provided as an argument or read from stdin.
50
+
51
+ Content can be provided in two ways:
52
+ 1. Using the --content parameter
53
+ 2. Piping content through stdin (if --content is not provided)
54
+
55
+ Examples:
56
+
57
+ # Using content parameter
58
+ basic-memory tools write-note --title "My Note" --folder "notes" --content "Note content"
59
+
60
+ # Using stdin pipe
61
+ echo "# My Note Content" | basic-memory tools write-note --title "My Note" --folder "notes"
62
+
63
+ # Using heredoc
64
+ cat << EOF | basic-memory tools write-note --title "My Note" --folder "notes"
65
+ # My Document
66
+
67
+ This is my document content.
68
+
69
+ - Point 1
70
+ - Point 2
71
+ EOF
72
+
73
+ # Reading from a file
74
+ cat document.md | basic-memory tools write-note --title "Document" --folder "docs"
75
+ """
40
76
  try:
77
+ # If content is not provided, read from stdin
78
+ if content is None:
79
+ # Check if we're getting data from a pipe or redirect
80
+ if not sys.stdin.isatty():
81
+ content = sys.stdin.read()
82
+ else: # pragma: no cover
83
+ # If stdin is a terminal (no pipe/redirect), inform the user
84
+ typer.echo(
85
+ "No content provided. Please provide content via --content or by piping to stdin.",
86
+ err=True,
87
+ )
88
+ raise typer.Exit(1)
89
+
90
+ # Also check for empty content
91
+ if content is not None and not content.strip():
92
+ typer.echo("Empty content provided. Please provide non-empty content.", err=True)
93
+ raise typer.Exit(1)
94
+
41
95
  note = asyncio.run(mcp_write_note(title, content, folder, tags))
42
96
  rprint(note)
43
97
  except Exception as e: # pragma: no cover
@@ -79,7 +133,11 @@ def build_context(
79
133
  max_related=max_related,
80
134
  )
81
135
  )
82
- rprint(context.model_dump_json(indent=2))
136
+ # Use json module for more controlled serialization
137
+ import json
138
+
139
+ context_dict = context.model_dump(exclude_none=True)
140
+ print(json.dumps(context_dict, indent=2, ensure_ascii=True, default=str))
83
141
  except Exception as e: # pragma: no cover
84
142
  if not isinstance(e, typer.Exit):
85
143
  typer.echo(f"Error during build_context: {e}", err=True)
@@ -107,7 +165,11 @@ def recent_activity(
107
165
  max_related=max_related,
108
166
  )
109
167
  )
110
- rprint(context.model_dump_json(indent=2))
168
+ # Use json module for more controlled serialization
169
+ import json
170
+
171
+ context_dict = context.model_dump(exclude_none=True)
172
+ print(json.dumps(context_dict, indent=2, ensure_ascii=True, default=str))
111
173
  except Exception as e: # pragma: no cover
112
174
  if not isinstance(e, typer.Exit):
113
175
  typer.echo(f"Error during build_context: {e}", err=True)
@@ -139,7 +201,11 @@ def search(
139
201
  after_date=after_date,
140
202
  )
141
203
  results = asyncio.run(mcp_search(query=search_query, page=page, page_size=page_size))
142
- rprint(results.model_dump_json(indent=2))
204
+ # Use json module for more controlled serialization
205
+ import json
206
+
207
+ results_dict = results.model_dump(exclude_none=True)
208
+ print(json.dumps(results_dict, indent=2, ensure_ascii=True, default=str))
143
209
  except Exception as e: # pragma: no cover
144
210
  if not isinstance(e, typer.Exit):
145
211
  logger.exception("Error during search", e)
@@ -148,18 +214,6 @@ def search(
148
214
  raise
149
215
 
150
216
 
151
- @tool_app.command()
152
- def get_entity(identifier: str):
153
- try:
154
- entity = asyncio.run(mcp_get_entity(identifier=identifier))
155
- rprint(entity.model_dump_json(indent=2))
156
- except Exception as e: # pragma: no cover
157
- if not isinstance(e, typer.Exit):
158
- typer.echo(f"Error during get_entity: {e}", err=True)
159
- raise typer.Exit(1)
160
- raise
161
-
162
-
163
217
  @tool_app.command(name="continue-conversation")
164
218
  def continue_conversation(
165
219
  topic: Annotated[Optional[str], typer.Option(help="Topic or keyword to search for")] = None,
@@ -167,7 +221,7 @@ def continue_conversation(
167
221
  Optional[str], typer.Option(help="How far back to look for activity")
168
222
  ] = None,
169
223
  ):
170
- """Continue a previous conversation or work session."""
224
+ """Prompt to continue a previous conversation or work session."""
171
225
  try:
172
226
  # Prompt functions return formatted strings directly
173
227
  session = asyncio.run(mcp_continue_conversation(topic=topic, timeframe=timeframe))
@@ -178,3 +232,22 @@ def continue_conversation(
178
232
  typer.echo(f"Error continuing conversation: {e}", err=True)
179
233
  raise typer.Exit(1)
180
234
  raise
235
+
236
+
237
+ # @tool_app.command(name="show-recent-activity")
238
+ # def show_recent_activity(
239
+ # timeframe: Annotated[
240
+ # str, typer.Option(help="How far back to look for activity")
241
+ # ] = "7d",
242
+ # ):
243
+ # """Prompt to show recent activity."""
244
+ # try:
245
+ # # Prompt functions return formatted strings directly
246
+ # session = asyncio.run(recent_activity_prompt(timeframe=timeframe))
247
+ # rprint(session)
248
+ # except Exception as e: # pragma: no cover
249
+ # if not isinstance(e, typer.Exit):
250
+ # logger.exception("Error continuing conversation", e)
251
+ # typer.echo(f"Error continuing conversation: {e}", err=True)
252
+ # raise typer.Exit(1)
253
+ # raise
basic_memory/cli/main.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """Main CLI entry point for basic-memory.""" # pragma: no cover
2
2
 
3
3
  from basic_memory.cli.app import app # pragma: no cover
4
+ import typer
4
5
 
5
6
  # Register commands
6
7
  from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
@@ -12,8 +13,46 @@ from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
12
13
  import_claude_conversations,
13
14
  import_claude_projects,
14
15
  import_chatgpt,
15
- tools,
16
+ tool,
17
+ project,
16
18
  )
17
19
 
20
+
21
+ # Version command
22
+ @app.callback(invoke_without_command=True)
23
+ def main(
24
+ ctx: typer.Context,
25
+ project: str = typer.Option( # noqa
26
+ "main",
27
+ "--project",
28
+ "-p",
29
+ help="Specify which project to use",
30
+ envvar="BASIC_MEMORY_PROJECT",
31
+ ),
32
+ version: bool = typer.Option(
33
+ False,
34
+ "--version",
35
+ "-V",
36
+ help="Show version information and exit.",
37
+ is_eager=True,
38
+ ),
39
+ ):
40
+ """Basic Memory - Local-first personal knowledge management system."""
41
+ if version: # pragma: no cover
42
+ from basic_memory import __version__
43
+ from basic_memory.config import config
44
+
45
+ typer.echo(f"Basic Memory v{__version__}")
46
+ typer.echo(f"Current project: {config.project}")
47
+ typer.echo(f"Project path: {config.home}")
48
+ raise typer.Exit()
49
+
50
+ # Handle project selection via environment variable
51
+ if project:
52
+ import os
53
+
54
+ os.environ["BASIC_MEMORY_PROJECT"] = project
55
+
56
+
18
57
  if __name__ == "__main__": # pragma: no cover
19
58
  app()