basic-memory 0.14.3__py3-none-any.whl → 0.15.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.
- basic_memory/__init__.py +1 -1
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
- basic_memory/api/app.py +10 -4
- basic_memory/api/routers/knowledge_router.py +25 -8
- basic_memory/api/routers/project_router.py +99 -4
- basic_memory/api/routers/resource_router.py +3 -3
- basic_memory/cli/app.py +9 -28
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/cloud/__init__.py +5 -0
- basic_memory/cli/commands/cloud/api_client.py +112 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +818 -0
- basic_memory/cli/commands/cloud/core_commands.py +288 -0
- basic_memory/cli/commands/cloud/mount_commands.py +295 -0
- basic_memory/cli/commands/cloud/rclone_config.py +288 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +198 -0
- basic_memory/cli/commands/command_utils.py +60 -0
- basic_memory/cli/commands/import_memory_json.py +0 -4
- basic_memory/cli/commands/mcp.py +16 -4
- basic_memory/cli/commands/project.py +141 -145
- basic_memory/cli/commands/status.py +34 -22
- basic_memory/cli/commands/sync.py +45 -228
- basic_memory/cli/commands/tool.py +87 -16
- basic_memory/cli/main.py +1 -0
- basic_memory/config.py +96 -20
- basic_memory/db.py +104 -3
- basic_memory/deps.py +20 -3
- basic_memory/file_utils.py +89 -0
- basic_memory/ignore_utils.py +295 -0
- basic_memory/importers/chatgpt_importer.py +1 -1
- basic_memory/importers/utils.py +2 -2
- basic_memory/markdown/entity_parser.py +2 -2
- basic_memory/markdown/markdown_processor.py +2 -2
- basic_memory/markdown/plugins.py +39 -21
- basic_memory/markdown/utils.py +1 -1
- basic_memory/mcp/async_client.py +22 -10
- basic_memory/mcp/project_context.py +141 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +49 -4
- basic_memory/mcp/prompts/continue_conversation.py +1 -1
- basic_memory/mcp/prompts/recent_activity.py +116 -32
- basic_memory/mcp/prompts/search.py +1 -1
- basic_memory/mcp/prompts/utils.py +11 -4
- basic_memory/mcp/resources/ai_assistant_guide.md +179 -41
- basic_memory/mcp/resources/project_info.py +20 -6
- basic_memory/mcp/server.py +0 -37
- basic_memory/mcp/tools/__init__.py +5 -6
- basic_memory/mcp/tools/build_context.py +39 -19
- basic_memory/mcp/tools/canvas.py +19 -8
- basic_memory/mcp/tools/chatgpt_tools.py +178 -0
- basic_memory/mcp/tools/delete_note.py +67 -34
- basic_memory/mcp/tools/edit_note.py +55 -39
- basic_memory/mcp/tools/headers.py +44 -0
- basic_memory/mcp/tools/list_directory.py +18 -8
- basic_memory/mcp/tools/move_note.py +119 -41
- basic_memory/mcp/tools/project_management.py +77 -229
- basic_memory/mcp/tools/read_content.py +28 -12
- basic_memory/mcp/tools/read_note.py +97 -57
- basic_memory/mcp/tools/recent_activity.py +441 -42
- basic_memory/mcp/tools/search.py +82 -70
- basic_memory/mcp/tools/sync_status.py +5 -4
- basic_memory/mcp/tools/utils.py +19 -0
- basic_memory/mcp/tools/view_note.py +31 -6
- basic_memory/mcp/tools/write_note.py +65 -14
- basic_memory/models/knowledge.py +19 -2
- basic_memory/models/project.py +6 -2
- basic_memory/repository/entity_repository.py +31 -84
- basic_memory/repository/project_repository.py +1 -1
- basic_memory/repository/relation_repository.py +13 -0
- basic_memory/repository/repository.py +2 -2
- basic_memory/repository/search_repository.py +9 -3
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/base.py +70 -12
- basic_memory/schemas/cloud.py +46 -0
- basic_memory/schemas/memory.py +99 -18
- basic_memory/schemas/project_info.py +9 -10
- basic_memory/schemas/sync_report.py +48 -0
- basic_memory/services/context_service.py +35 -11
- basic_memory/services/directory_service.py +7 -0
- basic_memory/services/entity_service.py +82 -52
- basic_memory/services/initialization.py +30 -11
- basic_memory/services/project_service.py +23 -33
- basic_memory/sync/sync_service.py +148 -24
- basic_memory/sync/watch_service.py +128 -44
- basic_memory/utils.py +181 -109
- {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/METADATA +26 -96
- basic_memory-0.15.0.dist-info/RECORD +147 -0
- basic_memory/mcp/project_session.py +0 -120
- basic_memory-0.14.3.dist-info/RECORD +0 -132
- {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,242 +1,59 @@
|
|
|
1
1
|
"""Command module for basic-memory sync operations."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
from
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import List, Dict
|
|
4
|
+
from typing import Annotated, Optional
|
|
8
5
|
|
|
9
6
|
import typer
|
|
10
|
-
from loguru import logger
|
|
11
|
-
from rich.console import Console
|
|
12
|
-
from rich.tree import Tree
|
|
13
7
|
|
|
14
|
-
from basic_memory import db
|
|
15
8
|
from basic_memory.cli.app import app
|
|
16
|
-
from basic_memory.
|
|
17
|
-
from basic_memory.
|
|
18
|
-
from basic_memory.markdown.markdown_processor import MarkdownProcessor
|
|
19
|
-
from basic_memory.models import Project
|
|
20
|
-
from basic_memory.repository import (
|
|
21
|
-
EntityRepository,
|
|
22
|
-
ObservationRepository,
|
|
23
|
-
RelationRepository,
|
|
24
|
-
ProjectRepository,
|
|
25
|
-
)
|
|
26
|
-
from basic_memory.repository.search_repository import SearchRepository
|
|
27
|
-
from basic_memory.services import EntityService, FileService
|
|
28
|
-
from basic_memory.services.link_resolver import LinkResolver
|
|
29
|
-
from basic_memory.services.search_service import SearchService
|
|
30
|
-
from basic_memory.sync import SyncService
|
|
31
|
-
from basic_memory.sync.sync_service import SyncReport
|
|
32
|
-
|
|
33
|
-
console = Console()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@dataclass
|
|
37
|
-
class ValidationIssue:
|
|
38
|
-
file_path: str
|
|
39
|
-
error: str
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
async def get_sync_service(project: Project) -> SyncService: # pragma: no cover
|
|
43
|
-
"""Get sync service instance with all dependencies."""
|
|
44
|
-
|
|
45
|
-
app_config = ConfigManager().config
|
|
46
|
-
_, session_maker = await db.get_or_create_db(
|
|
47
|
-
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
project_path = Path(project.path)
|
|
51
|
-
entity_parser = EntityParser(project_path)
|
|
52
|
-
markdown_processor = MarkdownProcessor(entity_parser)
|
|
53
|
-
file_service = FileService(project_path, markdown_processor)
|
|
54
|
-
|
|
55
|
-
# Initialize repositories
|
|
56
|
-
entity_repository = EntityRepository(session_maker, project_id=project.id)
|
|
57
|
-
observation_repository = ObservationRepository(session_maker, project_id=project.id)
|
|
58
|
-
relation_repository = RelationRepository(session_maker, project_id=project.id)
|
|
59
|
-
search_repository = SearchRepository(session_maker, project_id=project.id)
|
|
60
|
-
|
|
61
|
-
# Initialize services
|
|
62
|
-
search_service = SearchService(search_repository, entity_repository, file_service)
|
|
63
|
-
link_resolver = LinkResolver(entity_repository, search_service)
|
|
64
|
-
|
|
65
|
-
# Initialize services
|
|
66
|
-
entity_service = EntityService(
|
|
67
|
-
entity_parser,
|
|
68
|
-
entity_repository,
|
|
69
|
-
observation_repository,
|
|
70
|
-
relation_repository,
|
|
71
|
-
file_service,
|
|
72
|
-
link_resolver,
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
# Create sync service
|
|
76
|
-
sync_service = SyncService(
|
|
77
|
-
app_config=app_config,
|
|
78
|
-
entity_service=entity_service,
|
|
79
|
-
entity_parser=entity_parser,
|
|
80
|
-
entity_repository=entity_repository,
|
|
81
|
-
relation_repository=relation_repository,
|
|
82
|
-
search_service=search_service,
|
|
83
|
-
file_service=file_service,
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
return sync_service
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def group_issues_by_directory(issues: List[ValidationIssue]) -> Dict[str, List[ValidationIssue]]:
|
|
90
|
-
"""Group validation issues by directory."""
|
|
91
|
-
grouped = defaultdict(list)
|
|
92
|
-
for issue in issues:
|
|
93
|
-
dir_name = Path(issue.file_path).parent.name
|
|
94
|
-
grouped[dir_name].append(issue)
|
|
95
|
-
return dict(grouped)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def display_sync_summary(knowledge: SyncReport):
|
|
99
|
-
"""Display a one-line summary of sync changes."""
|
|
100
|
-
config = get_project_config()
|
|
101
|
-
total_changes = knowledge.total
|
|
102
|
-
project_name = config.project
|
|
103
|
-
|
|
104
|
-
if total_changes == 0:
|
|
105
|
-
console.print(f"[green]Project '{project_name}': Everything up to date[/green]")
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
# Format as: "Synced X files (A new, B modified, C moved, D deleted)"
|
|
109
|
-
changes = []
|
|
110
|
-
new_count = len(knowledge.new)
|
|
111
|
-
mod_count = len(knowledge.modified)
|
|
112
|
-
move_count = len(knowledge.moves)
|
|
113
|
-
del_count = len(knowledge.deleted)
|
|
114
|
-
|
|
115
|
-
if new_count:
|
|
116
|
-
changes.append(f"[green]{new_count} new[/green]")
|
|
117
|
-
if mod_count:
|
|
118
|
-
changes.append(f"[yellow]{mod_count} modified[/yellow]")
|
|
119
|
-
if move_count:
|
|
120
|
-
changes.append(f"[blue]{move_count} moved[/blue]")
|
|
121
|
-
if del_count:
|
|
122
|
-
changes.append(f"[red]{del_count} deleted[/red]")
|
|
123
|
-
|
|
124
|
-
console.print(f"Project '{project_name}': Synced {total_changes} files ({', '.join(changes)})")
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def display_detailed_sync_results(knowledge: SyncReport):
|
|
128
|
-
"""Display detailed sync results with trees."""
|
|
129
|
-
config = get_project_config()
|
|
130
|
-
project_name = config.project
|
|
131
|
-
|
|
132
|
-
if knowledge.total == 0:
|
|
133
|
-
console.print(f"\n[green]Project '{project_name}': Everything up to date[/green]")
|
|
134
|
-
return
|
|
135
|
-
|
|
136
|
-
console.print(f"\n[bold]Sync Results for Project '{project_name}'[/bold]")
|
|
137
|
-
|
|
138
|
-
if knowledge.total > 0:
|
|
139
|
-
knowledge_tree = Tree("[bold]Knowledge Files[/bold]")
|
|
140
|
-
if knowledge.new:
|
|
141
|
-
created = knowledge_tree.add("[green]Created[/green]")
|
|
142
|
-
for path in sorted(knowledge.new):
|
|
143
|
-
checksum = knowledge.checksums.get(path, "")
|
|
144
|
-
created.add(f"[green]{path}[/green] ({checksum[:8]})")
|
|
145
|
-
if knowledge.modified:
|
|
146
|
-
modified = knowledge_tree.add("[yellow]Modified[/yellow]")
|
|
147
|
-
for path in sorted(knowledge.modified):
|
|
148
|
-
checksum = knowledge.checksums.get(path, "")
|
|
149
|
-
modified.add(f"[yellow]{path}[/yellow] ({checksum[:8]})")
|
|
150
|
-
if knowledge.moves:
|
|
151
|
-
moved = knowledge_tree.add("[blue]Moved[/blue]")
|
|
152
|
-
for old_path, new_path in sorted(knowledge.moves.items()):
|
|
153
|
-
checksum = knowledge.checksums.get(new_path, "")
|
|
154
|
-
moved.add(f"[blue]{old_path}[/blue] → [blue]{new_path}[/blue] ({checksum[:8]})")
|
|
155
|
-
if knowledge.deleted:
|
|
156
|
-
deleted = knowledge_tree.add("[red]Deleted[/red]")
|
|
157
|
-
for path in sorted(knowledge.deleted):
|
|
158
|
-
deleted.add(f"[red]{path}[/red]")
|
|
159
|
-
console.print(knowledge_tree)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
async def run_sync(verbose: bool = False):
|
|
163
|
-
"""Run sync operation."""
|
|
164
|
-
app_config = ConfigManager().config
|
|
165
|
-
config = get_project_config()
|
|
166
|
-
|
|
167
|
-
_, session_maker = await db.get_or_create_db(
|
|
168
|
-
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
|
169
|
-
)
|
|
170
|
-
project_repository = ProjectRepository(session_maker)
|
|
171
|
-
project = await project_repository.get_by_name(config.project)
|
|
172
|
-
if not project: # pragma: no cover
|
|
173
|
-
raise Exception(f"Project '{config.project}' not found")
|
|
174
|
-
|
|
175
|
-
import time
|
|
176
|
-
|
|
177
|
-
start_time = time.time()
|
|
178
|
-
|
|
179
|
-
logger.info(
|
|
180
|
-
"Sync command started",
|
|
181
|
-
project=config.project,
|
|
182
|
-
verbose=verbose,
|
|
183
|
-
directory=str(config.home),
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
sync_service = await get_sync_service(project)
|
|
187
|
-
|
|
188
|
-
logger.info("Running one-time sync")
|
|
189
|
-
knowledge_changes = await sync_service.sync(config.home, project_name=project.name)
|
|
190
|
-
|
|
191
|
-
# Log results
|
|
192
|
-
duration_ms = int((time.time() - start_time) * 1000)
|
|
193
|
-
logger.info(
|
|
194
|
-
"Sync command completed",
|
|
195
|
-
project=config.project,
|
|
196
|
-
total_changes=knowledge_changes.total,
|
|
197
|
-
new_files=len(knowledge_changes.new),
|
|
198
|
-
modified_files=len(knowledge_changes.modified),
|
|
199
|
-
deleted_files=len(knowledge_changes.deleted),
|
|
200
|
-
moved_files=len(knowledge_changes.moves),
|
|
201
|
-
duration_ms=duration_ms,
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
# Display results
|
|
205
|
-
if verbose:
|
|
206
|
-
display_detailed_sync_results(knowledge_changes)
|
|
207
|
-
else:
|
|
208
|
-
display_sync_summary(knowledge_changes) # pragma: no cover
|
|
9
|
+
from basic_memory.cli.commands.command_utils import run_sync
|
|
10
|
+
from basic_memory.config import ConfigManager
|
|
209
11
|
|
|
210
12
|
|
|
211
13
|
@app.command()
|
|
212
14
|
def sync(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
"
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
15
|
+
project: Annotated[
|
|
16
|
+
Optional[str],
|
|
17
|
+
typer.Option(help="The project name."),
|
|
18
|
+
] = None,
|
|
19
|
+
watch: Annotated[
|
|
20
|
+
bool,
|
|
21
|
+
typer.Option("--watch", help="Run continuous sync (cloud mode only)"),
|
|
22
|
+
] = False,
|
|
23
|
+
interval: Annotated[
|
|
24
|
+
int,
|
|
25
|
+
typer.Option("--interval", help="Sync interval in seconds for watch mode (default: 60)"),
|
|
26
|
+
] = 60,
|
|
219
27
|
) -> None:
|
|
220
|
-
"""Sync knowledge files with the database.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
28
|
+
"""Sync knowledge files with the database.
|
|
29
|
+
|
|
30
|
+
In local mode: Scans filesystem and updates database.
|
|
31
|
+
In cloud mode: Runs bidirectional file sync (bisync) then updates database.
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
bm sync # One-time sync
|
|
35
|
+
bm sync --watch # Continuous sync every 60s
|
|
36
|
+
bm sync --watch --interval 30 # Continuous sync every 30s
|
|
37
|
+
"""
|
|
38
|
+
config = ConfigManager().config
|
|
39
|
+
|
|
40
|
+
if config.cloud_mode_enabled:
|
|
41
|
+
# Cloud mode: run bisync which includes database sync
|
|
42
|
+
from basic_memory.cli.commands.cloud.bisync_commands import run_bisync, run_bisync_watch
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
if watch:
|
|
46
|
+
run_bisync_watch(interval_seconds=interval)
|
|
47
|
+
else:
|
|
48
|
+
run_bisync()
|
|
49
|
+
except Exception:
|
|
50
|
+
raise typer.Exit(1)
|
|
51
|
+
else:
|
|
52
|
+
# Local mode: just database sync
|
|
53
|
+
if watch:
|
|
54
|
+
typer.echo(
|
|
55
|
+
"Error: --watch is only available in cloud mode. Run 'bm cloud login' first."
|
|
239
56
|
)
|
|
240
|
-
typer.echo(f"Error during sync: {e}", err=True)
|
|
241
57
|
raise typer.Exit(1)
|
|
242
|
-
|
|
58
|
+
|
|
59
|
+
asyncio.run(run_sync(project))
|
|
@@ -9,6 +9,7 @@ from loguru import logger
|
|
|
9
9
|
from rich import print as rprint
|
|
10
10
|
|
|
11
11
|
from basic_memory.cli.app import app
|
|
12
|
+
from basic_memory.config import ConfigManager
|
|
12
13
|
|
|
13
14
|
# Import prompts
|
|
14
15
|
from basic_memory.mcp.prompts.continue_conversation import (
|
|
@@ -34,6 +35,12 @@ app.add_typer(tool_app, name="tool", help="Access to MCP tools via CLI")
|
|
|
34
35
|
def write_note(
|
|
35
36
|
title: Annotated[str, typer.Option(help="The title of the note")],
|
|
36
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,
|
|
37
44
|
content: Annotated[
|
|
38
45
|
Optional[str],
|
|
39
46
|
typer.Option(
|
|
@@ -90,7 +97,19 @@ def write_note(
|
|
|
90
97
|
typer.echo("Empty content provided. Please provide non-empty content.", err=True)
|
|
91
98
|
raise typer.Exit(1)
|
|
92
99
|
|
|
93
|
-
|
|
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))
|
|
94
113
|
rprint(note)
|
|
95
114
|
except Exception as e: # pragma: no cover
|
|
96
115
|
if not isinstance(e, typer.Exit):
|
|
@@ -100,10 +119,33 @@ def write_note(
|
|
|
100
119
|
|
|
101
120
|
|
|
102
121
|
@tool_app.command()
|
|
103
|
-
def read_note(
|
|
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
|
+
):
|
|
104
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
|
+
|
|
105
147
|
try:
|
|
106
|
-
note = asyncio.run(mcp_read_note.fn(identifier, page, page_size))
|
|
148
|
+
note = asyncio.run(mcp_read_note.fn(identifier, project_name, page, page_size))
|
|
107
149
|
rprint(note)
|
|
108
150
|
except Exception as e: # pragma: no cover
|
|
109
151
|
if not isinstance(e, typer.Exit):
|
|
@@ -115,6 +157,10 @@ def read_note(identifier: str, page: int = 1, page_size: int = 10):
|
|
|
115
157
|
@tool_app.command()
|
|
116
158
|
def build_context(
|
|
117
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,
|
|
118
164
|
depth: Optional[int] = 1,
|
|
119
165
|
timeframe: Optional[TimeFrame] = "7d",
|
|
120
166
|
page: int = 1,
|
|
@@ -122,9 +168,23 @@ def build_context(
|
|
|
122
168
|
max_related: int = 10,
|
|
123
169
|
):
|
|
124
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
|
+
|
|
125
184
|
try:
|
|
126
185
|
context = asyncio.run(
|
|
127
186
|
mcp_build_context.fn(
|
|
187
|
+
project=project_name,
|
|
128
188
|
url=url,
|
|
129
189
|
depth=depth,
|
|
130
190
|
timeframe=timeframe,
|
|
@@ -150,30 +210,21 @@ def recent_activity(
|
|
|
150
210
|
type: Annotated[Optional[List[SearchItemType]], typer.Option()] = None,
|
|
151
211
|
depth: Optional[int] = 1,
|
|
152
212
|
timeframe: Optional[TimeFrame] = "7d",
|
|
153
|
-
page: int = 1,
|
|
154
|
-
page_size: int = 10,
|
|
155
|
-
max_related: int = 10,
|
|
156
213
|
):
|
|
157
214
|
"""Get recent activity across the knowledge base."""
|
|
158
215
|
try:
|
|
159
|
-
|
|
216
|
+
result = asyncio.run(
|
|
160
217
|
mcp_recent_activity.fn(
|
|
161
218
|
type=type, # pyright: ignore [reportArgumentType]
|
|
162
219
|
depth=depth,
|
|
163
220
|
timeframe=timeframe,
|
|
164
|
-
page=page,
|
|
165
|
-
page_size=page_size,
|
|
166
|
-
max_related=max_related,
|
|
167
221
|
)
|
|
168
222
|
)
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
context_dict = context.model_dump(exclude_none=True)
|
|
173
|
-
print(json.dumps(context_dict, indent=2, ensure_ascii=True, default=str))
|
|
223
|
+
# The tool now returns a formatted string directly
|
|
224
|
+
print(result)
|
|
174
225
|
except Exception as e: # pragma: no cover
|
|
175
226
|
if not isinstance(e, typer.Exit):
|
|
176
|
-
typer.echo(f"Error during
|
|
227
|
+
typer.echo(f"Error during recent_activity: {e}", err=True)
|
|
177
228
|
raise typer.Exit(1)
|
|
178
229
|
raise
|
|
179
230
|
|
|
@@ -183,6 +234,12 @@ def search_notes(
|
|
|
183
234
|
query: str,
|
|
184
235
|
permalink: Annotated[bool, typer.Option("--permalink", help="Search permalink values")] = False,
|
|
185
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,
|
|
186
243
|
after_date: Annotated[
|
|
187
244
|
Optional[str],
|
|
188
245
|
typer.Option("--after_date", help="Search results after date, eg. '2d', '1 week'"),
|
|
@@ -191,6 +248,19 @@ def search_notes(
|
|
|
191
248
|
page_size: int = 10,
|
|
192
249
|
):
|
|
193
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
|
+
|
|
194
264
|
if permalink and title: # pragma: no cover
|
|
195
265
|
print("Cannot search both permalink and title")
|
|
196
266
|
raise typer.Abort()
|
|
@@ -212,6 +282,7 @@ def search_notes(
|
|
|
212
282
|
results = asyncio.run(
|
|
213
283
|
mcp_search.fn(
|
|
214
284
|
query,
|
|
285
|
+
project_name,
|
|
215
286
|
search_type=search_type,
|
|
216
287
|
page=page,
|
|
217
288
|
after_date=after_date,
|
basic_memory/cli/main.py
CHANGED
basic_memory/config.py
CHANGED
|
@@ -46,7 +46,7 @@ class BasicMemoryConfig(BaseSettings):
|
|
|
46
46
|
|
|
47
47
|
projects: Dict[str, str] = Field(
|
|
48
48
|
default_factory=lambda: {
|
|
49
|
-
"main":
|
|
49
|
+
"main": Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory")).as_posix()
|
|
50
50
|
},
|
|
51
51
|
description="Mapping of project names to their filesystem paths",
|
|
52
52
|
)
|
|
@@ -54,6 +54,10 @@ class BasicMemoryConfig(BaseSettings):
|
|
|
54
54
|
default="main",
|
|
55
55
|
description="Name of the default project to use",
|
|
56
56
|
)
|
|
57
|
+
default_project_mode: bool = Field(
|
|
58
|
+
default=False,
|
|
59
|
+
description="When True, MCP tools automatically use default_project when no project parameter is specified. Enables simplified UX for single-project workflows.",
|
|
60
|
+
)
|
|
57
61
|
|
|
58
62
|
# overridden by ~/.basic-memory/config.json
|
|
59
63
|
log_level: str = "INFO"
|
|
@@ -63,6 +67,10 @@ class BasicMemoryConfig(BaseSettings):
|
|
|
63
67
|
default=1000, description="Milliseconds to wait after changes before syncing", gt=0
|
|
64
68
|
)
|
|
65
69
|
|
|
70
|
+
watch_project_reload_interval: int = Field(
|
|
71
|
+
default=30, description="Seconds between reloading project list in watch service", gt=0
|
|
72
|
+
)
|
|
73
|
+
|
|
66
74
|
# update permalinks on move
|
|
67
75
|
update_permalinks_on_move: bool = Field(
|
|
68
76
|
default=False,
|
|
@@ -74,17 +82,83 @@ class BasicMemoryConfig(BaseSettings):
|
|
|
74
82
|
description="Whether to sync changes in real time. default (True)",
|
|
75
83
|
)
|
|
76
84
|
|
|
85
|
+
sync_thread_pool_size: int = Field(
|
|
86
|
+
default=4,
|
|
87
|
+
description="Size of thread pool for file I/O operations in sync service",
|
|
88
|
+
gt=0,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
kebab_filenames: bool = Field(
|
|
92
|
+
default=False,
|
|
93
|
+
description="Format for generated filenames. False preserves spaces and special chars, True converts them to hyphens for consistency with permalinks",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
disable_permalinks: bool = Field(
|
|
97
|
+
default=False,
|
|
98
|
+
description="Disable automatic permalink generation in frontmatter. When enabled, new notes won't have permalinks added and sync won't update permalinks. Existing permalinks will still work for reading.",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
skip_initialization_sync: bool = Field(
|
|
102
|
+
default=False,
|
|
103
|
+
description="Skip expensive initialization synchronization. Useful for cloud/stateless deployments where project reconciliation is not needed.",
|
|
104
|
+
)
|
|
105
|
+
|
|
77
106
|
# API connection configuration
|
|
78
107
|
api_url: Optional[str] = Field(
|
|
79
108
|
default=None,
|
|
80
109
|
description="URL of remote Basic Memory API. If set, MCP will connect to this API instead of using local ASGI transport.",
|
|
81
110
|
)
|
|
82
111
|
|
|
112
|
+
# Cloud configuration
|
|
113
|
+
cloud_client_id: str = Field(
|
|
114
|
+
default="client_01K6KWQPW6J1M8VV7R3TZP5A6M",
|
|
115
|
+
description="OAuth client ID for Basic Memory Cloud",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
cloud_domain: str = Field(
|
|
119
|
+
default="https://eloquent-lotus-05.authkit.app",
|
|
120
|
+
description="AuthKit domain for Basic Memory Cloud",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
cloud_host: str = Field(
|
|
124
|
+
default_factory=lambda: os.getenv(
|
|
125
|
+
"BASIC_MEMORY_CLOUD_HOST", "https://cloud.basicmemory.com"
|
|
126
|
+
),
|
|
127
|
+
description="Basic Memory Cloud host URL",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
cloud_mode: bool = Field(
|
|
131
|
+
default=False,
|
|
132
|
+
description="Enable cloud mode - all requests go to cloud instead of local (config file value)",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def cloud_mode_enabled(self) -> bool:
|
|
137
|
+
"""Check if cloud mode is enabled.
|
|
138
|
+
|
|
139
|
+
Priority:
|
|
140
|
+
1. BASIC_MEMORY_CLOUD_MODE environment variable
|
|
141
|
+
2. Config file value (cloud_mode)
|
|
142
|
+
"""
|
|
143
|
+
env_value = os.environ.get("BASIC_MEMORY_CLOUD_MODE", "").lower()
|
|
144
|
+
if env_value in ("true", "1", "yes"):
|
|
145
|
+
return True
|
|
146
|
+
elif env_value in ("false", "0", "no"):
|
|
147
|
+
return False
|
|
148
|
+
# Fall back to config file value
|
|
149
|
+
return self.cloud_mode
|
|
150
|
+
|
|
151
|
+
bisync_config: Dict[str, Any] = Field(
|
|
152
|
+
default_factory=lambda: {
|
|
153
|
+
"profile": "balanced",
|
|
154
|
+
"sync_dir": str(Path.home() / "basic-memory-cloud-sync"),
|
|
155
|
+
},
|
|
156
|
+
description="Bisync configuration for cloud sync",
|
|
157
|
+
)
|
|
158
|
+
|
|
83
159
|
model_config = SettingsConfigDict(
|
|
84
160
|
env_prefix="BASIC_MEMORY_",
|
|
85
161
|
extra="ignore",
|
|
86
|
-
env_file=".env",
|
|
87
|
-
env_file_encoding="utf-8",
|
|
88
162
|
)
|
|
89
163
|
|
|
90
164
|
def get_project_path(self, project_name: Optional[str] = None) -> Path: # pragma: no cover
|
|
@@ -100,9 +174,9 @@ class BasicMemoryConfig(BaseSettings):
|
|
|
100
174
|
"""Ensure configuration is valid after initialization."""
|
|
101
175
|
# Ensure main project exists
|
|
102
176
|
if "main" not in self.projects: # pragma: no cover
|
|
103
|
-
self.projects["main"] =
|
|
177
|
+
self.projects["main"] = (
|
|
104
178
|
Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory"))
|
|
105
|
-
)
|
|
179
|
+
).as_posix()
|
|
106
180
|
|
|
107
181
|
# Ensure default project is valid
|
|
108
182
|
if self.default_project not in self.projects: # pragma: no cover
|
|
@@ -153,6 +227,10 @@ class BasicMemoryConfig(BaseSettings):
|
|
|
153
227
|
raise e
|
|
154
228
|
return v
|
|
155
229
|
|
|
230
|
+
@property
|
|
231
|
+
def data_dir_path(self):
|
|
232
|
+
return Path.home() / DATA_DIR_NAME
|
|
233
|
+
|
|
156
234
|
|
|
157
235
|
class ConfigManager:
|
|
158
236
|
"""Manages Basic Memory configuration."""
|
|
@@ -215,7 +293,7 @@ class ConfigManager:
|
|
|
215
293
|
|
|
216
294
|
# Load config, modify it, and save it
|
|
217
295
|
config = self.load_config()
|
|
218
|
-
config.projects[name] =
|
|
296
|
+
config.projects[name] = project_path.as_posix()
|
|
219
297
|
self.save_config(config)
|
|
220
298
|
return ProjectConfig(name=name, home=project_path)
|
|
221
299
|
|
|
@@ -242,7 +320,7 @@ class ConfigManager:
|
|
|
242
320
|
|
|
243
321
|
# Load config, modify, and save
|
|
244
322
|
config = self.load_config()
|
|
245
|
-
config.default_project =
|
|
323
|
+
config.default_project = project_name
|
|
246
324
|
self.save_config(config)
|
|
247
325
|
|
|
248
326
|
def get_project(self, name: str) -> Tuple[str, str] | Tuple[None, None]:
|
|
@@ -271,7 +349,7 @@ def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
|
|
|
271
349
|
os_project_name = os.environ.get("BASIC_MEMORY_PROJECT", None)
|
|
272
350
|
if os_project_name: # pragma: no cover
|
|
273
351
|
logger.warning(
|
|
274
|
-
f"BASIC_MEMORY_PROJECT is not supported anymore.
|
|
352
|
+
f"BASIC_MEMORY_PROJECT is not supported anymore. Set the default project in the config instead. Setting default project to {os_project_name}"
|
|
275
353
|
)
|
|
276
354
|
actual_project_name = project_name
|
|
277
355
|
# if the project_name is passed in, use it
|
|
@@ -302,15 +380,6 @@ def save_basic_memory_config(file_path: Path, config: BasicMemoryConfig) -> None
|
|
|
302
380
|
logger.error(f"Failed to save config: {e}")
|
|
303
381
|
|
|
304
382
|
|
|
305
|
-
def update_current_project(project_name: str) -> None:
|
|
306
|
-
"""Update the global config to use a different project.
|
|
307
|
-
|
|
308
|
-
This is used by the CLI when --project flag is specified.
|
|
309
|
-
"""
|
|
310
|
-
global config
|
|
311
|
-
config = get_project_config(project_name) # pragma: no cover
|
|
312
|
-
|
|
313
|
-
|
|
314
383
|
# setup logging to a single log file in user home directory
|
|
315
384
|
user_home = Path.home()
|
|
316
385
|
log_dir = user_home / DATA_DIR_NAME
|
|
@@ -351,15 +420,22 @@ def setup_basic_memory_logging(): # pragma: no cover
|
|
|
351
420
|
# print("Skipping duplicate logging setup")
|
|
352
421
|
return
|
|
353
422
|
|
|
354
|
-
# Check for console logging environment variable
|
|
355
|
-
|
|
423
|
+
# Check for console logging environment variable - accept more truthy values
|
|
424
|
+
console_logging_env = os.getenv("BASIC_MEMORY_CONSOLE_LOGGING", "false").lower()
|
|
425
|
+
console_logging = console_logging_env in ("true", "1", "yes", "on")
|
|
426
|
+
|
|
427
|
+
# Check for log level environment variable first, fall back to config
|
|
428
|
+
log_level = os.getenv("BASIC_MEMORY_LOG_LEVEL")
|
|
429
|
+
if not log_level:
|
|
430
|
+
config_manager = ConfigManager()
|
|
431
|
+
log_level = config_manager.config.log_level
|
|
356
432
|
|
|
357
433
|
config_manager = ConfigManager()
|
|
358
434
|
config = get_project_config()
|
|
359
435
|
setup_logging(
|
|
360
436
|
env=config_manager.config.env,
|
|
361
437
|
home_dir=user_home, # Use user home for logs
|
|
362
|
-
log_level=
|
|
438
|
+
log_level=log_level,
|
|
363
439
|
log_file=f"{DATA_DIR_NAME}/basic-memory-{process_name}.log",
|
|
364
440
|
console=console_logging,
|
|
365
441
|
)
|