basic-memory 0.7.0__py3-none-any.whl → 0.17.4__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.
- basic_memory/__init__.py +5 -1
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +130 -20
- basic_memory/alembic/migrations.py +4 -9
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
- basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
- basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
- basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
- basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
- basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
- basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
- basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
- basic_memory/api/app.py +87 -20
- basic_memory/api/container.py +133 -0
- basic_memory/api/routers/__init__.py +4 -1
- basic_memory/api/routers/directory_router.py +84 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +180 -23
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +9 -64
- basic_memory/api/routers/project_router.py +460 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +136 -11
- basic_memory/api/routers/search_router.py +5 -5
- basic_memory/api/routers/utils.py +169 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/api/v2/__init__.py +35 -0
- basic_memory/api/v2/routers/__init__.py +21 -0
- basic_memory/api/v2/routers/directory_router.py +93 -0
- basic_memory/api/v2/routers/importer_router.py +181 -0
- basic_memory/api/v2/routers/knowledge_router.py +427 -0
- basic_memory/api/v2/routers/memory_router.py +130 -0
- basic_memory/api/v2/routers/project_router.py +359 -0
- basic_memory/api/v2/routers/prompt_router.py +269 -0
- basic_memory/api/v2/routers/resource_router.py +286 -0
- basic_memory/api/v2/routers/search_router.py +73 -0
- basic_memory/cli/app.py +80 -10
- basic_memory/cli/auth.py +300 -0
- basic_memory/cli/commands/__init__.py +15 -2
- basic_memory/cli/commands/cloud/__init__.py +6 -0
- basic_memory/cli/commands/cloud/api_client.py +127 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
- basic_memory/cli/commands/cloud/cloud_utils.py +108 -0
- basic_memory/cli/commands/cloud/core_commands.py +195 -0
- basic_memory/cli/commands/cloud/rclone_commands.py +397 -0
- basic_memory/cli/commands/cloud/rclone_config.py +110 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
- basic_memory/cli/commands/cloud/upload.py +240 -0
- basic_memory/cli/commands/cloud/upload_command.py +124 -0
- basic_memory/cli/commands/command_utils.py +99 -0
- basic_memory/cli/commands/db.py +87 -12
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +47 -223
- basic_memory/cli/commands/import_claude_conversations.py +48 -171
- basic_memory/cli/commands/import_claude_projects.py +53 -160
- basic_memory/cli/commands/import_memory_json.py +55 -111
- basic_memory/cli/commands/mcp.py +67 -11
- basic_memory/cli/commands/project.py +889 -0
- basic_memory/cli/commands/status.py +52 -34
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/container.py +84 -0
- basic_memory/cli/main.py +14 -6
- basic_memory/config.py +580 -26
- basic_memory/db.py +285 -28
- basic_memory/deps/__init__.py +293 -0
- basic_memory/deps/config.py +26 -0
- basic_memory/deps/db.py +56 -0
- basic_memory/deps/importers.py +200 -0
- basic_memory/deps/projects.py +238 -0
- basic_memory/deps/repositories.py +179 -0
- basic_memory/deps/services.py +480 -0
- basic_memory/deps.py +16 -185
- basic_memory/file_utils.py +318 -54
- basic_memory/ignore_utils.py +297 -0
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +100 -0
- basic_memory/importers/chatgpt_importer.py +245 -0
- basic_memory/importers/claude_conversations_importer.py +192 -0
- basic_memory/importers/claude_projects_importer.py +184 -0
- basic_memory/importers/memory_json_importer.py +128 -0
- basic_memory/importers/utils.py +61 -0
- basic_memory/markdown/entity_parser.py +182 -23
- basic_memory/markdown/markdown_processor.py +70 -7
- basic_memory/markdown/plugins.py +43 -23
- basic_memory/markdown/schemas.py +1 -1
- basic_memory/markdown/utils.py +38 -14
- basic_memory/mcp/async_client.py +135 -4
- basic_memory/mcp/clients/__init__.py +28 -0
- basic_memory/mcp/clients/directory.py +70 -0
- basic_memory/mcp/clients/knowledge.py +176 -0
- basic_memory/mcp/clients/memory.py +120 -0
- basic_memory/mcp/clients/project.py +89 -0
- basic_memory/mcp/clients/resource.py +71 -0
- basic_memory/mcp/clients/search.py +65 -0
- basic_memory/mcp/container.py +110 -0
- basic_memory/mcp/project_context.py +155 -0
- basic_memory/mcp/prompts/__init__.py +19 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
- basic_memory/mcp/prompts/continue_conversation.py +62 -0
- basic_memory/mcp/prompts/recent_activity.py +188 -0
- basic_memory/mcp/prompts/search.py +57 -0
- basic_memory/mcp/prompts/utils.py +162 -0
- basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
- basic_memory/mcp/resources/project_info.py +71 -0
- basic_memory/mcp/server.py +61 -9
- basic_memory/mcp/tools/__init__.py +33 -21
- basic_memory/mcp/tools/build_context.py +120 -0
- basic_memory/mcp/tools/canvas.py +152 -0
- basic_memory/mcp/tools/chatgpt_tools.py +190 -0
- basic_memory/mcp/tools/delete_note.py +249 -0
- basic_memory/mcp/tools/edit_note.py +325 -0
- basic_memory/mcp/tools/list_directory.py +157 -0
- basic_memory/mcp/tools/move_note.py +549 -0
- basic_memory/mcp/tools/project_management.py +204 -0
- basic_memory/mcp/tools/read_content.py +281 -0
- basic_memory/mcp/tools/read_note.py +265 -0
- basic_memory/mcp/tools/recent_activity.py +528 -0
- basic_memory/mcp/tools/search.py +377 -24
- basic_memory/mcp/tools/utils.py +402 -16
- basic_memory/mcp/tools/view_note.py +78 -0
- basic_memory/mcp/tools/write_note.py +230 -0
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +82 -17
- basic_memory/models/project.py +93 -0
- basic_memory/models/search.py +68 -8
- basic_memory/project_resolver.py +222 -0
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +437 -8
- basic_memory/repository/observation_repository.py +36 -3
- basic_memory/repository/postgres_search_repository.py +451 -0
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +140 -0
- basic_memory/repository/relation_repository.py +79 -4
- basic_memory/repository/repository.py +148 -29
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +79 -268
- basic_memory/repository/search_repository_base.py +241 -0
- basic_memory/repository/sqlite_search_repository.py +437 -0
- basic_memory/runtime.py +61 -0
- basic_memory/schemas/__init__.py +22 -9
- basic_memory/schemas/base.py +131 -12
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/directory.py +31 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +194 -25
- basic_memory/schemas/project_info.py +213 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +56 -2
- basic_memory/schemas/response.py +85 -28
- basic_memory/schemas/search.py +36 -35
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/schemas/v2/__init__.py +27 -0
- basic_memory/schemas/v2/entity.py +133 -0
- basic_memory/schemas/v2/resource.py +47 -0
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +451 -138
- basic_memory/services/directory_service.py +310 -0
- basic_memory/services/entity_service.py +636 -71
- basic_memory/services/exceptions.py +21 -0
- basic_memory/services/file_service.py +402 -33
- basic_memory/services/initialization.py +216 -0
- basic_memory/services/link_resolver.py +50 -56
- basic_memory/services/project_service.py +888 -0
- basic_memory/services/search_service.py +232 -37
- basic_memory/sync/__init__.py +4 -2
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/coordinator.py +160 -0
- basic_memory/sync/sync_service.py +1200 -109
- basic_memory/sync/watch_service.py +432 -135
- basic_memory/telemetry.py +249 -0
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +407 -54
- basic_memory-0.17.4.dist-info/METADATA +617 -0
- basic_memory-0.17.4.dist-info/RECORD +193 -0
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +1 -0
- basic_memory/alembic/README +0 -1
- basic_memory/cli/commands/sync.py +0 -206
- basic_memory/cli/commands/tools.py +0 -157
- basic_memory/mcp/tools/knowledge.py +0 -68
- basic_memory/mcp/tools/memory.py +0 -170
- basic_memory/mcp/tools/notes.py +0 -202
- basic_memory/schemas/discovery.py +0 -28
- basic_memory/sync/file_change_scanner.py +0 -158
- basic_memory/sync/utils.py +0 -31
- basic_memory-0.7.0.dist-info/METADATA +0 -378
- basic_memory-0.7.0.dist-info/RECORD +0 -82
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,38 +1,25 @@
|
|
|
1
1
|
"""Status command for basic-memory CLI."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
from typing import Set, Dict
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
from mcp.server.fastmcp.exceptions import ToolError
|
|
7
7
|
import typer
|
|
8
8
|
from loguru import logger
|
|
9
9
|
from rich.console import Console
|
|
10
10
|
from rich.panel import Panel
|
|
11
11
|
from rich.tree import Tree
|
|
12
12
|
|
|
13
|
-
from basic_memory import db
|
|
14
13
|
from basic_memory.cli.app import app
|
|
15
|
-
from basic_memory.
|
|
16
|
-
from basic_memory.
|
|
17
|
-
from basic_memory.
|
|
18
|
-
from basic_memory.
|
|
19
|
-
from basic_memory.sync.utils import SyncReport
|
|
14
|
+
from basic_memory.mcp.async_client import get_client
|
|
15
|
+
from basic_memory.mcp.tools.utils import call_post
|
|
16
|
+
from basic_memory.schemas import SyncReportResponse
|
|
17
|
+
from basic_memory.mcp.project_context import get_active_project
|
|
20
18
|
|
|
21
19
|
# Create rich console
|
|
22
20
|
console = Console()
|
|
23
21
|
|
|
24
22
|
|
|
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
23
|
def add_files_to_tree(
|
|
37
24
|
tree: Tree, paths: Set[str], style: str, checksums: Dict[str, str] | None = None
|
|
38
25
|
):
|
|
@@ -60,7 +47,7 @@ def add_files_to_tree(
|
|
|
60
47
|
branch.add(f"[{style}]{file_name}[/{style}]")
|
|
61
48
|
|
|
62
49
|
|
|
63
|
-
def group_changes_by_directory(changes:
|
|
50
|
+
def group_changes_by_directory(changes: SyncReportResponse) -> Dict[str, Dict[str, int]]:
|
|
64
51
|
"""Group changes by directory for summary view."""
|
|
65
52
|
by_dir = {}
|
|
66
53
|
for change_type, paths in [
|
|
@@ -100,11 +87,13 @@ def build_directory_summary(counts: Dict[str, int]) -> str:
|
|
|
100
87
|
return " ".join(parts)
|
|
101
88
|
|
|
102
89
|
|
|
103
|
-
def display_changes(
|
|
90
|
+
def display_changes(
|
|
91
|
+
project_name: str, title: str, changes: SyncReportResponse, verbose: bool = False
|
|
92
|
+
):
|
|
104
93
|
"""Display changes using Rich for better visualization."""
|
|
105
|
-
tree = Tree(title)
|
|
94
|
+
tree = Tree(f"{project_name}: {title}")
|
|
106
95
|
|
|
107
|
-
if changes.
|
|
96
|
+
if changes.total == 0 and not changes.skipped_files:
|
|
108
97
|
tree.add("No changes")
|
|
109
98
|
console.print(Panel(tree, expand=False))
|
|
110
99
|
return
|
|
@@ -124,6 +113,13 @@ def display_changes(title: str, changes: SyncReport, verbose: bool = False):
|
|
|
124
113
|
if changes.deleted:
|
|
125
114
|
del_branch = tree.add("[red]Deleted[/red]")
|
|
126
115
|
add_files_to_tree(del_branch, changes.deleted, "red")
|
|
116
|
+
if changes.skipped_files:
|
|
117
|
+
skip_branch = tree.add("[red]! Skipped (Circuit Breaker)[/red]")
|
|
118
|
+
for skipped in sorted(changes.skipped_files, key=lambda x: x.path):
|
|
119
|
+
skip_branch.add(
|
|
120
|
+
f"[red]{skipped.path}[/red] "
|
|
121
|
+
f"(failures: {skipped.failure_count}, reason: {skipped.reason})"
|
|
122
|
+
)
|
|
127
123
|
else:
|
|
128
124
|
# Show directory summaries
|
|
129
125
|
by_dir = group_changes_by_directory(changes)
|
|
@@ -132,25 +128,47 @@ def display_changes(title: str, changes: SyncReport, verbose: bool = False):
|
|
|
132
128
|
if summary: # Only show directories with changes
|
|
133
129
|
tree.add(f"[bold]{dir_name}/[/bold] {summary}")
|
|
134
130
|
|
|
131
|
+
# Show skipped files summary in non-verbose mode
|
|
132
|
+
if changes.skipped_files:
|
|
133
|
+
skip_count = len(changes.skipped_files)
|
|
134
|
+
tree.add(
|
|
135
|
+
f"[red]! {skip_count} file{'s' if skip_count != 1 else ''} "
|
|
136
|
+
f"skipped due to repeated failures[/red]"
|
|
137
|
+
)
|
|
138
|
+
|
|
135
139
|
console.print(Panel(tree, expand=False))
|
|
136
140
|
|
|
137
141
|
|
|
138
|
-
async def run_status(
|
|
142
|
+
async def run_status(project: Optional[str] = None, verbose: bool = False): # pragma: no cover
|
|
139
143
|
"""Check sync status of files vs database."""
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
async with get_client() as client:
|
|
147
|
+
project_item = await get_active_project(client, project, None)
|
|
148
|
+
response = await call_post(client, f"{project_item.project_url}/project/status")
|
|
149
|
+
sync_report = SyncReportResponse.model_validate(response.json())
|
|
150
|
+
|
|
151
|
+
display_changes(project_item.name, "Status", sync_report, verbose)
|
|
152
|
+
|
|
153
|
+
except (ValueError, ToolError) as e:
|
|
154
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
155
|
+
raise typer.Exit(1)
|
|
143
156
|
|
|
144
157
|
|
|
145
158
|
@app.command()
|
|
146
159
|
def status(
|
|
160
|
+
project: Annotated[
|
|
161
|
+
Optional[str],
|
|
162
|
+
typer.Option(help="The project name."),
|
|
163
|
+
] = None,
|
|
147
164
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed file information"),
|
|
148
165
|
):
|
|
149
166
|
"""Show sync status between files and database."""
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
167
|
+
from basic_memory.cli.commands.command_utils import run_with_cleanup
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
run_with_cleanup(run_status(project, verbose)) # pragma: no cover
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(f"Error checking status: {e}")
|
|
173
|
+
typer.echo(f"Error checking status: {e}", err=True)
|
|
174
|
+
raise typer.Exit(code=1) # pragma: no cover
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Telemetry commands for basic-memory CLI."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
|
|
7
|
+
from basic_memory.cli.app import app
|
|
8
|
+
from basic_memory.config import ConfigManager
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
# Create telemetry subcommand group
|
|
13
|
+
telemetry_app = typer.Typer(help="Manage anonymous telemetry settings")
|
|
14
|
+
app.add_typer(telemetry_app, name="telemetry")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@telemetry_app.command("enable")
|
|
18
|
+
def enable() -> None:
|
|
19
|
+
"""Enable anonymous telemetry.
|
|
20
|
+
|
|
21
|
+
Telemetry helps improve Basic Memory by collecting anonymous usage data.
|
|
22
|
+
No personal data, note content, or file paths are ever collected.
|
|
23
|
+
"""
|
|
24
|
+
config_manager = ConfigManager()
|
|
25
|
+
config = config_manager.config
|
|
26
|
+
config.telemetry_enabled = True
|
|
27
|
+
config_manager.save_config(config)
|
|
28
|
+
console.print("[green]Telemetry enabled[/green]")
|
|
29
|
+
console.print("[dim]Thank you for helping improve Basic Memory![/dim]")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@telemetry_app.command("disable")
|
|
33
|
+
def disable() -> None:
|
|
34
|
+
"""Disable anonymous telemetry.
|
|
35
|
+
|
|
36
|
+
You can re-enable telemetry anytime with: bm telemetry enable
|
|
37
|
+
"""
|
|
38
|
+
config_manager = ConfigManager()
|
|
39
|
+
config = config_manager.config
|
|
40
|
+
config.telemetry_enabled = False
|
|
41
|
+
config_manager.save_config(config)
|
|
42
|
+
console.print("[yellow]Telemetry disabled[/yellow]")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@telemetry_app.command("status")
|
|
46
|
+
def status() -> None:
|
|
47
|
+
"""Show current telemetry status and what's collected."""
|
|
48
|
+
from basic_memory.telemetry import get_install_id, TELEMETRY_DOCS_URL
|
|
49
|
+
|
|
50
|
+
config = ConfigManager().config
|
|
51
|
+
|
|
52
|
+
status_text = (
|
|
53
|
+
"[green]enabled[/green]" if config.telemetry_enabled else "[yellow]disabled[/yellow]"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
console.print(f"\nTelemetry: {status_text}")
|
|
57
|
+
console.print(f"Install ID: [dim]{get_install_id()}[/dim]")
|
|
58
|
+
console.print()
|
|
59
|
+
|
|
60
|
+
what_we_collect = """
|
|
61
|
+
[bold]What we collect:[/bold]
|
|
62
|
+
- App version, Python version, OS, architecture
|
|
63
|
+
- Feature usage (which MCP tools and CLI commands)
|
|
64
|
+
- Sync statistics (entity count, duration)
|
|
65
|
+
- Error types (sanitized, no file paths)
|
|
66
|
+
|
|
67
|
+
[bold]What we NEVER collect:[/bold]
|
|
68
|
+
- Note content, file names, or paths
|
|
69
|
+
- Personal information
|
|
70
|
+
- IP addresses
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
console.print(
|
|
74
|
+
Panel(
|
|
75
|
+
what_we_collect.strip(),
|
|
76
|
+
title="Telemetry Details",
|
|
77
|
+
border_style="blue",
|
|
78
|
+
expand=False,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
console.print(f"[dim]Details: {TELEMETRY_DOCS_URL}[/dim]")
|
|
@@ -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
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""CLI composition root for Basic Memory.
|
|
2
|
+
|
|
3
|
+
This container owns reading ConfigManager and environment variables for the
|
|
4
|
+
CLI entrypoint. Downstream modules receive config/dependencies explicitly
|
|
5
|
+
rather than reading globals.
|
|
6
|
+
|
|
7
|
+
Design principles:
|
|
8
|
+
- Only this module reads ConfigManager directly
|
|
9
|
+
- Runtime mode (cloud/local/test) is resolved here
|
|
10
|
+
- Different CLI commands may need different initialization
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from basic_memory.config import BasicMemoryConfig, ConfigManager
|
|
16
|
+
from basic_memory.runtime import RuntimeMode, resolve_runtime_mode
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CliContainer:
|
|
21
|
+
"""Composition root for the CLI entrypoint.
|
|
22
|
+
|
|
23
|
+
Holds resolved configuration and runtime context.
|
|
24
|
+
Created once at CLI startup, then used by subcommands.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
config: BasicMemoryConfig
|
|
28
|
+
mode: RuntimeMode
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def create(cls) -> "CliContainer":
|
|
32
|
+
"""Create container by reading ConfigManager.
|
|
33
|
+
|
|
34
|
+
This is the single point where CLI reads global config.
|
|
35
|
+
"""
|
|
36
|
+
config = ConfigManager().config
|
|
37
|
+
mode = resolve_runtime_mode(
|
|
38
|
+
cloud_mode_enabled=config.cloud_mode_enabled,
|
|
39
|
+
is_test_env=config.is_test_env,
|
|
40
|
+
)
|
|
41
|
+
return cls(config=config, mode=mode)
|
|
42
|
+
|
|
43
|
+
# --- Runtime Mode Properties ---
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def is_cloud_mode(self) -> bool:
|
|
47
|
+
"""Whether running in cloud mode."""
|
|
48
|
+
return self.mode.is_cloud
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Module-level container instance (set by app callback)
|
|
52
|
+
_container: CliContainer | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_container() -> CliContainer:
|
|
56
|
+
"""Get the current CLI container.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The CLI container
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
RuntimeError: If container hasn't been initialized
|
|
63
|
+
"""
|
|
64
|
+
if _container is None:
|
|
65
|
+
raise RuntimeError("CLI container not initialized. Call set_container() first.")
|
|
66
|
+
return _container
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def set_container(container: CliContainer) -> None:
|
|
70
|
+
"""Set the CLI container (called by app callback)."""
|
|
71
|
+
global _container
|
|
72
|
+
_container = container
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_or_create_container() -> CliContainer:
|
|
76
|
+
"""Get existing container or create new one.
|
|
77
|
+
|
|
78
|
+
This is useful for CLI commands that might be called before
|
|
79
|
+
the main app callback runs (e.g., eager options).
|
|
80
|
+
"""
|
|
81
|
+
global _container
|
|
82
|
+
if _container is None:
|
|
83
|
+
_container = CliContainer.create()
|
|
84
|
+
return _container
|
basic_memory/cli/main.py
CHANGED
|
@@ -4,17 +4,25 @@ from basic_memory.cli.app import app # pragma: no cover
|
|
|
4
4
|
|
|
5
5
|
# Register commands
|
|
6
6
|
from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
|
|
7
|
-
|
|
8
|
-
sync,
|
|
7
|
+
cloud,
|
|
9
8
|
db,
|
|
10
|
-
|
|
11
|
-
mcp,
|
|
9
|
+
import_chatgpt,
|
|
12
10
|
import_claude_conversations,
|
|
13
11
|
import_claude_projects,
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
import_memory_json,
|
|
13
|
+
mcp,
|
|
14
|
+
project,
|
|
15
|
+
status,
|
|
16
|
+
telemetry,
|
|
17
|
+
tool,
|
|
16
18
|
)
|
|
17
19
|
|
|
20
|
+
# Re-apply warning filter AFTER all imports
|
|
21
|
+
# (authlib adds a DeprecationWarning filter that overrides ours)
|
|
22
|
+
import warnings # pragma: no cover
|
|
23
|
+
|
|
24
|
+
warnings.filterwarnings("ignore") # pragma: no cover
|
|
18
25
|
|
|
19
26
|
if __name__ == "__main__": # pragma: no cover
|
|
27
|
+
# start the app
|
|
20
28
|
app()
|