basic-memory 0.2.12__py3-none-any.whl → 0.16.1__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 (149) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +27 -3
  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/5fe1ab1ccebe_add_projects_table.py +108 -0
  7. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -0
  8. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  9. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  10. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  11. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +100 -0
  12. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  13. basic_memory/api/app.py +63 -31
  14. basic_memory/api/routers/__init__.py +4 -1
  15. basic_memory/api/routers/directory_router.py +84 -0
  16. basic_memory/api/routers/importer_router.py +152 -0
  17. basic_memory/api/routers/knowledge_router.py +165 -28
  18. basic_memory/api/routers/management_router.py +80 -0
  19. basic_memory/api/routers/memory_router.py +28 -67
  20. basic_memory/api/routers/project_router.py +406 -0
  21. basic_memory/api/routers/prompt_router.py +260 -0
  22. basic_memory/api/routers/resource_router.py +219 -14
  23. basic_memory/api/routers/search_router.py +21 -13
  24. basic_memory/api/routers/utils.py +130 -0
  25. basic_memory/api/template_loader.py +292 -0
  26. basic_memory/cli/app.py +52 -1
  27. basic_memory/cli/auth.py +277 -0
  28. basic_memory/cli/commands/__init__.py +13 -2
  29. basic_memory/cli/commands/cloud/__init__.py +6 -0
  30. basic_memory/cli/commands/cloud/api_client.py +112 -0
  31. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  32. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  33. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  34. basic_memory/cli/commands/cloud/rclone_commands.py +301 -0
  35. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  36. basic_memory/cli/commands/cloud/rclone_installer.py +249 -0
  37. basic_memory/cli/commands/cloud/upload.py +233 -0
  38. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  39. basic_memory/cli/commands/command_utils.py +51 -0
  40. basic_memory/cli/commands/db.py +26 -7
  41. basic_memory/cli/commands/import_chatgpt.py +83 -0
  42. basic_memory/cli/commands/import_claude_conversations.py +86 -0
  43. basic_memory/cli/commands/import_claude_projects.py +85 -0
  44. basic_memory/cli/commands/import_memory_json.py +35 -92
  45. basic_memory/cli/commands/mcp.py +84 -10
  46. basic_memory/cli/commands/project.py +876 -0
  47. basic_memory/cli/commands/status.py +47 -30
  48. basic_memory/cli/commands/tool.py +341 -0
  49. basic_memory/cli/main.py +13 -6
  50. basic_memory/config.py +481 -22
  51. basic_memory/db.py +192 -32
  52. basic_memory/deps.py +252 -22
  53. basic_memory/file_utils.py +113 -58
  54. basic_memory/ignore_utils.py +297 -0
  55. basic_memory/importers/__init__.py +27 -0
  56. basic_memory/importers/base.py +79 -0
  57. basic_memory/importers/chatgpt_importer.py +232 -0
  58. basic_memory/importers/claude_conversations_importer.py +177 -0
  59. basic_memory/importers/claude_projects_importer.py +148 -0
  60. basic_memory/importers/memory_json_importer.py +108 -0
  61. basic_memory/importers/utils.py +58 -0
  62. basic_memory/markdown/entity_parser.py +143 -23
  63. basic_memory/markdown/markdown_processor.py +3 -3
  64. basic_memory/markdown/plugins.py +39 -21
  65. basic_memory/markdown/schemas.py +1 -1
  66. basic_memory/markdown/utils.py +28 -13
  67. basic_memory/mcp/async_client.py +134 -4
  68. basic_memory/mcp/project_context.py +141 -0
  69. basic_memory/mcp/prompts/__init__.py +19 -0
  70. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  71. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  72. basic_memory/mcp/prompts/recent_activity.py +188 -0
  73. basic_memory/mcp/prompts/search.py +57 -0
  74. basic_memory/mcp/prompts/utils.py +162 -0
  75. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  76. basic_memory/mcp/resources/project_info.py +71 -0
  77. basic_memory/mcp/server.py +7 -13
  78. basic_memory/mcp/tools/__init__.py +33 -21
  79. basic_memory/mcp/tools/build_context.py +120 -0
  80. basic_memory/mcp/tools/canvas.py +130 -0
  81. basic_memory/mcp/tools/chatgpt_tools.py +187 -0
  82. basic_memory/mcp/tools/delete_note.py +225 -0
  83. basic_memory/mcp/tools/edit_note.py +320 -0
  84. basic_memory/mcp/tools/list_directory.py +167 -0
  85. basic_memory/mcp/tools/move_note.py +545 -0
  86. basic_memory/mcp/tools/project_management.py +200 -0
  87. basic_memory/mcp/tools/read_content.py +271 -0
  88. basic_memory/mcp/tools/read_note.py +255 -0
  89. basic_memory/mcp/tools/recent_activity.py +534 -0
  90. basic_memory/mcp/tools/search.py +369 -14
  91. basic_memory/mcp/tools/utils.py +374 -16
  92. basic_memory/mcp/tools/view_note.py +77 -0
  93. basic_memory/mcp/tools/write_note.py +207 -0
  94. basic_memory/models/__init__.py +3 -2
  95. basic_memory/models/knowledge.py +67 -15
  96. basic_memory/models/project.py +87 -0
  97. basic_memory/models/search.py +10 -6
  98. basic_memory/repository/__init__.py +2 -0
  99. basic_memory/repository/entity_repository.py +229 -7
  100. basic_memory/repository/observation_repository.py +35 -3
  101. basic_memory/repository/project_info_repository.py +10 -0
  102. basic_memory/repository/project_repository.py +103 -0
  103. basic_memory/repository/relation_repository.py +21 -2
  104. basic_memory/repository/repository.py +147 -29
  105. basic_memory/repository/search_repository.py +437 -59
  106. basic_memory/schemas/__init__.py +22 -9
  107. basic_memory/schemas/base.py +97 -8
  108. basic_memory/schemas/cloud.py +50 -0
  109. basic_memory/schemas/directory.py +30 -0
  110. basic_memory/schemas/importer.py +35 -0
  111. basic_memory/schemas/memory.py +188 -23
  112. basic_memory/schemas/project_info.py +211 -0
  113. basic_memory/schemas/prompt.py +90 -0
  114. basic_memory/schemas/request.py +57 -3
  115. basic_memory/schemas/response.py +9 -1
  116. basic_memory/schemas/search.py +33 -35
  117. basic_memory/schemas/sync_report.py +72 -0
  118. basic_memory/services/__init__.py +2 -1
  119. basic_memory/services/context_service.py +251 -106
  120. basic_memory/services/directory_service.py +295 -0
  121. basic_memory/services/entity_service.py +595 -60
  122. basic_memory/services/exceptions.py +21 -0
  123. basic_memory/services/file_service.py +284 -30
  124. basic_memory/services/initialization.py +191 -0
  125. basic_memory/services/link_resolver.py +50 -56
  126. basic_memory/services/project_service.py +863 -0
  127. basic_memory/services/search_service.py +172 -34
  128. basic_memory/sync/__init__.py +3 -2
  129. basic_memory/sync/background_sync.py +26 -0
  130. basic_memory/sync/sync_service.py +1176 -96
  131. basic_memory/sync/watch_service.py +412 -135
  132. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  133. basic_memory/templates/prompts/search.hbs +101 -0
  134. basic_memory/utils.py +388 -28
  135. basic_memory-0.16.1.dist-info/METADATA +493 -0
  136. basic_memory-0.16.1.dist-info/RECORD +148 -0
  137. {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/entry_points.txt +1 -0
  138. basic_memory/alembic/README +0 -1
  139. basic_memory/cli/commands/sync.py +0 -203
  140. basic_memory/mcp/tools/knowledge.py +0 -56
  141. basic_memory/mcp/tools/memory.py +0 -151
  142. basic_memory/mcp/tools/notes.py +0 -122
  143. basic_memory/schemas/discovery.py +0 -28
  144. basic_memory/sync/file_change_scanner.py +0 -158
  145. basic_memory/sync/utils.py +0 -34
  146. basic_memory-0.2.12.dist-info/METADATA +0 -291
  147. basic_memory-0.2.12.dist-info/RECORD +0 -78
  148. {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/WHEEL +0 -0
  149. {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/licenses/LICENSE +0 -0
@@ -2,38 +2,25 @@
2
2
 
3
3
  import asyncio
4
4
  from typing import Set, Dict
5
+ from typing import Annotated, Optional
5
6
 
7
+ from mcp.server.fastmcp.exceptions import ToolError
6
8
  import typer
7
9
  from loguru import logger
8
10
  from rich.console import Console
9
11
  from rich.panel import Panel
10
12
  from rich.tree import Tree
11
13
 
12
- from basic_memory import db
13
14
  from basic_memory.cli.app import app
14
- from basic_memory.config import config
15
- from basic_memory.db import DatabaseType
16
- from basic_memory.repository import EntityRepository
17
- from basic_memory.sync import FileChangeScanner
18
- from basic_memory.sync.utils import SyncReport
15
+ from basic_memory.mcp.async_client import get_client
16
+ from basic_memory.mcp.tools.utils import call_post
17
+ from basic_memory.schemas import SyncReportResponse
18
+ from basic_memory.mcp.project_context import get_active_project
19
19
 
20
20
  # Create rich console
21
21
  console = Console()
22
22
 
23
23
 
24
- async def get_file_change_scanner(
25
- db_type=DatabaseType.FILESYSTEM,
26
- ) -> FileChangeScanner: # pragma: no cover
27
- """Get sync service instance."""
28
- async with db.engine_session_factory(db_path=config.database_path, db_type=db_type) as (
29
- engine,
30
- session_maker,
31
- ):
32
- entity_repository = EntityRepository(session_maker)
33
- file_change_scanner = FileChangeScanner(entity_repository)
34
- return file_change_scanner
35
-
36
-
37
24
  def add_files_to_tree(
38
25
  tree: Tree, paths: Set[str], style: str, checksums: Dict[str, str] | None = None
39
26
  ):
@@ -61,7 +48,7 @@ def add_files_to_tree(
61
48
  branch.add(f"[{style}]{file_name}[/{style}]")
62
49
 
63
50
 
64
- def group_changes_by_directory(changes: SyncReport) -> Dict[str, Dict[str, int]]:
51
+ def group_changes_by_directory(changes: SyncReportResponse) -> Dict[str, Dict[str, int]]:
65
52
  """Group changes by directory for summary view."""
66
53
  by_dir = {}
67
54
  for change_type, paths in [
@@ -101,11 +88,13 @@ def build_directory_summary(counts: Dict[str, int]) -> str:
101
88
  return " ".join(parts)
102
89
 
103
90
 
104
- def display_changes(title: str, changes: SyncReport, verbose: bool = False):
91
+ def display_changes(
92
+ project_name: str, title: str, changes: SyncReportResponse, verbose: bool = False
93
+ ):
105
94
  """Display changes using Rich for better visualization."""
106
- tree = Tree(title)
95
+ tree = Tree(f"{project_name}: {title}")
107
96
 
108
- if changes.total_changes == 0:
97
+ if changes.total == 0 and not changes.skipped_files:
109
98
  tree.add("No changes")
110
99
  console.print(Panel(tree, expand=False))
111
100
  return
@@ -125,6 +114,13 @@ def display_changes(title: str, changes: SyncReport, verbose: bool = False):
125
114
  if changes.deleted:
126
115
  del_branch = tree.add("[red]Deleted[/red]")
127
116
  add_files_to_tree(del_branch, changes.deleted, "red")
117
+ if changes.skipped_files:
118
+ skip_branch = tree.add("[red]! Skipped (Circuit Breaker)[/red]")
119
+ for skipped in sorted(changes.skipped_files, key=lambda x: x.path):
120
+ skip_branch.add(
121
+ f"[red]{skipped.path}[/red] "
122
+ f"(failures: {skipped.failure_count}, reason: {skipped.reason})"
123
+ )
128
124
  else:
129
125
  # Show directory summaries
130
126
  by_dir = group_changes_by_directory(changes)
@@ -133,24 +129,45 @@ def display_changes(title: str, changes: SyncReport, verbose: bool = False):
133
129
  if summary: # Only show directories with changes
134
130
  tree.add(f"[bold]{dir_name}/[/bold] {summary}")
135
131
 
132
+ # Show skipped files summary in non-verbose mode
133
+ if changes.skipped_files:
134
+ skip_count = len(changes.skipped_files)
135
+ tree.add(
136
+ f"[red]! {skip_count} file{'s' if skip_count != 1 else ''} "
137
+ f"skipped due to repeated failures[/red]"
138
+ )
139
+
136
140
  console.print(Panel(tree, expand=False))
137
141
 
138
142
 
139
- async def run_status(sync_service: FileChangeScanner, verbose: bool = False):
143
+ async def run_status(project: Optional[str] = None, verbose: bool = False): # pragma: no cover
140
144
  """Check sync status of files vs database."""
141
- # Check knowledge/ directory
142
- knowledge_changes = await sync_service.find_knowledge_changes(config.home)
143
- display_changes("Knowledge Files", knowledge_changes, verbose)
145
+
146
+ try:
147
+ async with get_client() as client:
148
+ project_item = await get_active_project(client, project, None)
149
+ response = await call_post(client, f"{project_item.project_url}/project/status")
150
+ sync_report = SyncReportResponse.model_validate(response.json())
151
+
152
+ display_changes(project_item.name, "Status", sync_report, verbose)
153
+
154
+ except (ValueError, ToolError) as e:
155
+ console.print(f"[red]Error: {e}[/red]")
156
+ raise typer.Exit(1)
144
157
 
145
158
 
146
159
  @app.command()
147
160
  def status(
161
+ project: Annotated[
162
+ Optional[str],
163
+ typer.Option(help="The project name."),
164
+ ] = None,
148
165
  verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed file information"),
149
166
  ):
150
167
  """Show sync status between files and database."""
151
168
  try:
152
- sync_service = asyncio.run(get_file_change_scanner())
153
- asyncio.run(run_status(sync_service, verbose)) # pragma: no cover
169
+ asyncio.run(run_status(project, verbose)) # pragma: no cover
154
170
  except Exception as e:
155
- logger.exception(f"Error checking status: {e}")
171
+ logger.error(f"Error checking status: {e}")
172
+ typer.echo(f"Error checking status: {e}", err=True)
156
173
  raise typer.Exit(code=1) # pragma: no cover
@@ -0,0 +1,341 @@
1
+ """CLI tool commands for Basic Memory."""
2
+
3
+ import asyncio
4
+ import sys
5
+ from typing import Annotated, List, Optional
6
+
7
+ import typer
8
+ from loguru import logger
9
+ from rich import print as rprint
10
+
11
+ from basic_memory.cli.app import app
12
+ from basic_memory.config import ConfigManager
13
+
14
+ # Import prompts
15
+ from basic_memory.mcp.prompts.continue_conversation import (
16
+ continue_conversation as mcp_continue_conversation,
17
+ )
18
+ from basic_memory.mcp.prompts.recent_activity import (
19
+ recent_activity_prompt as recent_activity_prompt,
20
+ )
21
+ from basic_memory.mcp.tools import build_context as mcp_build_context
22
+ from basic_memory.mcp.tools import read_note as mcp_read_note
23
+ from basic_memory.mcp.tools import recent_activity as mcp_recent_activity
24
+ from basic_memory.mcp.tools import search_notes as mcp_search
25
+ from basic_memory.mcp.tools import write_note as mcp_write_note
26
+ from basic_memory.schemas.base import TimeFrame
27
+ from basic_memory.schemas.memory import MemoryUrl
28
+ from basic_memory.schemas.search import SearchItemType
29
+
30
+ tool_app = typer.Typer()
31
+ app.add_typer(tool_app, name="tool", help="Access to MCP tools via CLI")
32
+
33
+
34
+ @tool_app.command()
35
+ def write_note(
36
+ title: Annotated[str, typer.Option(help="The title of the note")],
37
+ folder: Annotated[str, typer.Option(help="The folder to create the note in")],
38
+ project: Annotated[
39
+ Optional[str],
40
+ typer.Option(
41
+ help="The project to write to. If not provided, the default project will be used."
42
+ ),
43
+ ] = None,
44
+ content: Annotated[
45
+ Optional[str],
46
+ typer.Option(
47
+ 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"
48
+ ),
49
+ ] = None,
50
+ tags: Annotated[
51
+ Optional[List[str]], typer.Option(help="A list of tags to apply to the note")
52
+ ] = None,
53
+ ):
54
+ """Create or update a markdown note. Content can be provided as an argument or read from stdin.
55
+
56
+ Content can be provided in two ways:
57
+ 1. Using the --content parameter
58
+ 2. Piping content through stdin (if --content is not provided)
59
+
60
+ Examples:
61
+
62
+ # Using content parameter
63
+ basic-memory tools write-note --title "My Note" --folder "notes" --content "Note content"
64
+
65
+ # Using stdin pipe
66
+ echo "# My Note Content" | basic-memory tools write-note --title "My Note" --folder "notes"
67
+
68
+ # Using heredoc
69
+ cat << EOF | basic-memory tools write-note --title "My Note" --folder "notes"
70
+ # My Document
71
+
72
+ This is my document content.
73
+
74
+ - Point 1
75
+ - Point 2
76
+ EOF
77
+
78
+ # Reading from a file
79
+ cat document.md | basic-memory tools write-note --title "Document" --folder "docs"
80
+ """
81
+ try:
82
+ # If content is not provided, read from stdin
83
+ if content is None:
84
+ # Check if we're getting data from a pipe or redirect
85
+ if not sys.stdin.isatty():
86
+ content = sys.stdin.read()
87
+ else: # pragma: no cover
88
+ # If stdin is a terminal (no pipe/redirect), inform the user
89
+ typer.echo(
90
+ "No content provided. Please provide content via --content or by piping to stdin.",
91
+ err=True,
92
+ )
93
+ raise typer.Exit(1)
94
+
95
+ # Also check for empty content
96
+ if content is not None and not content.strip():
97
+ typer.echo("Empty content provided. Please provide non-empty content.", err=True)
98
+ raise typer.Exit(1)
99
+
100
+ # look for the project in the config
101
+ config_manager = ConfigManager()
102
+ project_name = None
103
+ if project is not None:
104
+ project_name, _ = config_manager.get_project(project)
105
+ if not project_name:
106
+ typer.echo(f"No project found named: {project}", err=True)
107
+ raise typer.Exit(1)
108
+
109
+ # use the project name, or the default from the config
110
+ project_name = project_name or config_manager.default_project
111
+
112
+ note = asyncio.run(mcp_write_note.fn(title, content, folder, project_name, tags))
113
+ rprint(note)
114
+ except Exception as e: # pragma: no cover
115
+ if not isinstance(e, typer.Exit):
116
+ typer.echo(f"Error during write_note: {e}", err=True)
117
+ raise typer.Exit(1)
118
+ raise
119
+
120
+
121
+ @tool_app.command()
122
+ def read_note(
123
+ identifier: str,
124
+ project: Annotated[
125
+ Optional[str],
126
+ typer.Option(
127
+ help="The project to use for the note. If not provided, the default project will be used."
128
+ ),
129
+ ] = None,
130
+ page: int = 1,
131
+ page_size: int = 10,
132
+ ):
133
+ """Read a markdown note from the knowledge base."""
134
+
135
+ # look for the project in the config
136
+ config_manager = ConfigManager()
137
+ project_name = None
138
+ if project is not None:
139
+ project_name, _ = config_manager.get_project(project)
140
+ if not project_name:
141
+ typer.echo(f"No project found named: {project}", err=True)
142
+ raise typer.Exit(1)
143
+
144
+ # use the project name, or the default from the config
145
+ project_name = project_name or config_manager.default_project
146
+
147
+ try:
148
+ note = asyncio.run(mcp_read_note.fn(identifier, project_name, page, page_size))
149
+ rprint(note)
150
+ except Exception as e: # pragma: no cover
151
+ if not isinstance(e, typer.Exit):
152
+ typer.echo(f"Error during read_note: {e}", err=True)
153
+ raise typer.Exit(1)
154
+ raise
155
+
156
+
157
+ @tool_app.command()
158
+ def build_context(
159
+ url: MemoryUrl,
160
+ project: Annotated[
161
+ Optional[str],
162
+ typer.Option(help="The project to use. If not provided, the default project will be used."),
163
+ ] = None,
164
+ depth: Optional[int] = 1,
165
+ timeframe: Optional[TimeFrame] = "7d",
166
+ page: int = 1,
167
+ page_size: int = 10,
168
+ max_related: int = 10,
169
+ ):
170
+ """Get context needed to continue a discussion."""
171
+
172
+ # look for the project in the config
173
+ config_manager = ConfigManager()
174
+ project_name = None
175
+ if project is not None:
176
+ project_name, _ = config_manager.get_project(project)
177
+ if not project_name:
178
+ typer.echo(f"No project found named: {project}", err=True)
179
+ raise typer.Exit(1)
180
+
181
+ # use the project name, or the default from the config
182
+ project_name = project_name or config_manager.default_project
183
+
184
+ try:
185
+ context = asyncio.run(
186
+ mcp_build_context.fn(
187
+ project=project_name,
188
+ url=url,
189
+ depth=depth,
190
+ timeframe=timeframe,
191
+ page=page,
192
+ page_size=page_size,
193
+ max_related=max_related,
194
+ )
195
+ )
196
+ # Use json module for more controlled serialization
197
+ import json
198
+
199
+ context_dict = context.model_dump(exclude_none=True)
200
+ print(json.dumps(context_dict, indent=2, ensure_ascii=True, default=str))
201
+ except Exception as e: # pragma: no cover
202
+ if not isinstance(e, typer.Exit):
203
+ typer.echo(f"Error during build_context: {e}", err=True)
204
+ raise typer.Exit(1)
205
+ raise
206
+
207
+
208
+ @tool_app.command()
209
+ def recent_activity(
210
+ type: Annotated[Optional[List[SearchItemType]], typer.Option()] = None,
211
+ depth: Optional[int] = 1,
212
+ timeframe: Optional[TimeFrame] = "7d",
213
+ ):
214
+ """Get recent activity across the knowledge base."""
215
+ try:
216
+ result = asyncio.run(
217
+ mcp_recent_activity.fn(
218
+ type=type, # pyright: ignore [reportArgumentType]
219
+ depth=depth,
220
+ timeframe=timeframe,
221
+ )
222
+ )
223
+ # The tool now returns a formatted string directly
224
+ print(result)
225
+ except Exception as e: # pragma: no cover
226
+ if not isinstance(e, typer.Exit):
227
+ typer.echo(f"Error during recent_activity: {e}", err=True)
228
+ raise typer.Exit(1)
229
+ raise
230
+
231
+
232
+ @tool_app.command("search-notes")
233
+ def search_notes(
234
+ query: str,
235
+ permalink: Annotated[bool, typer.Option("--permalink", help="Search permalink values")] = False,
236
+ title: Annotated[bool, typer.Option("--title", help="Search title values")] = False,
237
+ project: Annotated[
238
+ Optional[str],
239
+ typer.Option(
240
+ help="The project to use for the note. If not provided, the default project will be used."
241
+ ),
242
+ ] = None,
243
+ after_date: Annotated[
244
+ Optional[str],
245
+ typer.Option("--after_date", help="Search results after date, eg. '2d', '1 week'"),
246
+ ] = None,
247
+ page: int = 1,
248
+ page_size: int = 10,
249
+ ):
250
+ """Search across all content in the knowledge base."""
251
+
252
+ # look for the project in the config
253
+ config_manager = ConfigManager()
254
+ project_name = None
255
+ if project is not None:
256
+ project_name, _ = config_manager.get_project(project)
257
+ if not project_name:
258
+ typer.echo(f"No project found named: {project}", err=True)
259
+ raise typer.Exit(1)
260
+
261
+ # use the project name, or the default from the config
262
+ project_name = project_name or config_manager.default_project
263
+
264
+ if permalink and title: # pragma: no cover
265
+ print("Cannot search both permalink and title")
266
+ raise typer.Abort()
267
+
268
+ try:
269
+ if permalink and title: # pragma: no cover
270
+ typer.echo(
271
+ "Use either --permalink or --title, not both. Exiting.",
272
+ err=True,
273
+ )
274
+ raise typer.Exit(1)
275
+
276
+ # set search type
277
+ search_type = ("permalink" if permalink else None,)
278
+ search_type = ("permalink_match" if permalink and "*" in query else None,)
279
+ search_type = ("title" if title else None,)
280
+ search_type = "text" if search_type is None else search_type
281
+
282
+ results = asyncio.run(
283
+ mcp_search.fn(
284
+ query,
285
+ project_name,
286
+ search_type=search_type,
287
+ page=page,
288
+ after_date=after_date,
289
+ page_size=page_size,
290
+ )
291
+ )
292
+ # Use json module for more controlled serialization
293
+ import json
294
+
295
+ results_dict = results.model_dump(exclude_none=True)
296
+ print(json.dumps(results_dict, indent=2, ensure_ascii=True, default=str))
297
+ except Exception as e: # pragma: no cover
298
+ if not isinstance(e, typer.Exit):
299
+ logger.exception("Error during search", e)
300
+ typer.echo(f"Error during search: {e}", err=True)
301
+ raise typer.Exit(1)
302
+ raise
303
+
304
+
305
+ @tool_app.command(name="continue-conversation")
306
+ def continue_conversation(
307
+ topic: Annotated[Optional[str], typer.Option(help="Topic or keyword to search for")] = None,
308
+ timeframe: Annotated[
309
+ Optional[str], typer.Option(help="How far back to look for activity")
310
+ ] = None,
311
+ ):
312
+ """Prompt to continue a previous conversation or work session."""
313
+ try:
314
+ # Prompt functions return formatted strings directly
315
+ session = asyncio.run(mcp_continue_conversation.fn(topic=topic, timeframe=timeframe)) # type: ignore
316
+ rprint(session)
317
+ except Exception as e: # pragma: no cover
318
+ if not isinstance(e, typer.Exit):
319
+ logger.exception("Error continuing conversation", e)
320
+ typer.echo(f"Error continuing conversation: {e}", err=True)
321
+ raise typer.Exit(1)
322
+ raise
323
+
324
+
325
+ # @tool_app.command(name="show-recent-activity")
326
+ # def show_recent_activity(
327
+ # timeframe: Annotated[
328
+ # str, typer.Option(help="How far back to look for activity")
329
+ # ] = "7d",
330
+ # ):
331
+ # """Prompt to show recent activity."""
332
+ # try:
333
+ # # Prompt functions return formatted strings directly
334
+ # session = asyncio.run(recent_activity_prompt(timeframe=timeframe))
335
+ # rprint(session)
336
+ # except Exception as e: # pragma: no cover
337
+ # if not isinstance(e, typer.Exit):
338
+ # logger.exception("Error continuing conversation", e)
339
+ # typer.echo(f"Error continuing conversation: {e}", err=True)
340
+ # raise typer.Exit(1)
341
+ # raise
basic_memory/cli/main.py CHANGED
@@ -1,14 +1,21 @@
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
- from basic_memory.utils import setup_logging # pragma: no cover
5
4
 
6
5
  # Register commands
7
- from basic_memory.cli.commands import status, sync, db, import_memory_json, mcp # noqa: F401 # pragma: no cover
8
-
9
-
10
- # Set up logging when module is imported
11
- setup_logging(log_file=".basic-memory/basic-memory-cli.log") # pragma: no cover
6
+ from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
7
+ cloud,
8
+ db,
9
+ import_chatgpt,
10
+ import_claude_conversations,
11
+ import_claude_projects,
12
+ import_memory_json,
13
+ mcp,
14
+ project,
15
+ status,
16
+ tool,
17
+ )
12
18
 
13
19
  if __name__ == "__main__": # pragma: no cover
20
+ # start the app
14
21
  app()