basic-memory 0.8.0__py3-none-any.whl → 0.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of basic-memory might be problematic. Click here for more details.
- basic_memory/__init__.py +1 -1
- basic_memory/alembic/migrations.py +4 -9
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +106 -0
- basic_memory/api/app.py +9 -6
- basic_memory/api/routers/__init__.py +2 -1
- basic_memory/api/routers/knowledge_router.py +30 -4
- basic_memory/api/routers/memory_router.py +3 -2
- basic_memory/api/routers/project_info_router.py +274 -0
- basic_memory/api/routers/search_router.py +22 -4
- basic_memory/cli/app.py +54 -3
- basic_memory/cli/commands/__init__.py +15 -2
- basic_memory/cli/commands/db.py +9 -13
- basic_memory/cli/commands/import_chatgpt.py +31 -36
- basic_memory/cli/commands/import_claude_conversations.py +32 -35
- basic_memory/cli/commands/import_claude_projects.py +34 -37
- basic_memory/cli/commands/import_memory_json.py +26 -28
- basic_memory/cli/commands/mcp.py +7 -1
- basic_memory/cli/commands/project.py +119 -0
- basic_memory/cli/commands/project_info.py +167 -0
- basic_memory/cli/commands/status.py +7 -9
- basic_memory/cli/commands/sync.py +54 -9
- basic_memory/cli/commands/{tools.py → tool.py} +92 -19
- basic_memory/cli/main.py +40 -1
- basic_memory/config.py +157 -10
- basic_memory/db.py +19 -4
- basic_memory/deps.py +10 -3
- basic_memory/file_utils.py +34 -18
- basic_memory/markdown/markdown_processor.py +1 -1
- basic_memory/markdown/utils.py +5 -0
- basic_memory/mcp/main.py +1 -2
- basic_memory/mcp/prompts/__init__.py +6 -2
- basic_memory/mcp/prompts/ai_assistant_guide.py +9 -10
- basic_memory/mcp/prompts/continue_conversation.py +65 -126
- basic_memory/mcp/prompts/recent_activity.py +55 -13
- basic_memory/mcp/prompts/search.py +72 -17
- basic_memory/mcp/prompts/utils.py +139 -82
- basic_memory/mcp/server.py +1 -1
- basic_memory/mcp/tools/__init__.py +11 -22
- basic_memory/mcp/tools/build_context.py +85 -0
- basic_memory/mcp/tools/canvas.py +17 -19
- basic_memory/mcp/tools/delete_note.py +28 -0
- basic_memory/mcp/tools/project_info.py +51 -0
- basic_memory/mcp/tools/{resource.py → read_content.py} +42 -5
- basic_memory/mcp/tools/read_note.py +190 -0
- basic_memory/mcp/tools/recent_activity.py +100 -0
- basic_memory/mcp/tools/search.py +56 -17
- basic_memory/mcp/tools/utils.py +245 -17
- basic_memory/mcp/tools/write_note.py +124 -0
- basic_memory/models/search.py +2 -1
- basic_memory/repository/entity_repository.py +3 -2
- basic_memory/repository/project_info_repository.py +9 -0
- basic_memory/repository/repository.py +23 -6
- basic_memory/repository/search_repository.py +33 -10
- basic_memory/schemas/__init__.py +12 -0
- basic_memory/schemas/memory.py +3 -2
- basic_memory/schemas/project_info.py +96 -0
- basic_memory/schemas/search.py +27 -32
- basic_memory/services/context_service.py +3 -3
- basic_memory/services/entity_service.py +8 -2
- basic_memory/services/file_service.py +107 -57
- basic_memory/services/link_resolver.py +5 -45
- basic_memory/services/search_service.py +45 -16
- basic_memory/sync/sync_service.py +274 -39
- basic_memory/sync/watch_service.py +174 -34
- basic_memory/utils.py +40 -40
- basic_memory-0.10.0.dist-info/METADATA +386 -0
- basic_memory-0.10.0.dist-info/RECORD +99 -0
- basic_memory/mcp/prompts/json_canvas_spec.py +0 -25
- basic_memory/mcp/tools/knowledge.py +0 -68
- basic_memory/mcp/tools/memory.py +0 -177
- basic_memory/mcp/tools/notes.py +0 -201
- basic_memory-0.8.0.dist-info/METADATA +0 -379
- basic_memory-0.8.0.dist-info/RECORD +0 -91
- {basic_memory-0.8.0.dist-info → basic_memory-0.10.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.8.0.dist-info → basic_memory-0.10.0.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.8.0.dist-info → basic_memory-0.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""CLI command for project info status."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.tree import Tree
|
|
12
|
+
|
|
13
|
+
from basic_memory.cli.app import app
|
|
14
|
+
from basic_memory.mcp.tools.project_info import project_info
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
info_app = typer.Typer()
|
|
18
|
+
app.add_typer(info_app, name="info", help="Get information about your Basic Memory project")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@info_app.command("stats")
|
|
22
|
+
def display_project_info(
|
|
23
|
+
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
24
|
+
):
|
|
25
|
+
"""Display detailed information and statistics about the current project."""
|
|
26
|
+
try:
|
|
27
|
+
# Get project info
|
|
28
|
+
info = asyncio.run(project_info())
|
|
29
|
+
|
|
30
|
+
if json_output:
|
|
31
|
+
# Convert to JSON and print
|
|
32
|
+
print(json.dumps(info.model_dump(), indent=2, default=str))
|
|
33
|
+
else:
|
|
34
|
+
# Create rich display
|
|
35
|
+
console = Console()
|
|
36
|
+
|
|
37
|
+
# Project configuration section
|
|
38
|
+
console.print(
|
|
39
|
+
Panel(
|
|
40
|
+
f"[bold]Project:[/bold] {info.project_name}\n"
|
|
41
|
+
f"[bold]Path:[/bold] {info.project_path}\n"
|
|
42
|
+
f"[bold]Default Project:[/bold] {info.default_project}\n",
|
|
43
|
+
title="📊 Basic Memory Project Info",
|
|
44
|
+
expand=False,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Statistics section
|
|
49
|
+
stats_table = Table(title="📈 Statistics")
|
|
50
|
+
stats_table.add_column("Metric", style="cyan")
|
|
51
|
+
stats_table.add_column("Count", style="green")
|
|
52
|
+
|
|
53
|
+
stats_table.add_row("Entities", str(info.statistics.total_entities))
|
|
54
|
+
stats_table.add_row("Observations", str(info.statistics.total_observations))
|
|
55
|
+
stats_table.add_row("Relations", str(info.statistics.total_relations))
|
|
56
|
+
stats_table.add_row(
|
|
57
|
+
"Unresolved Relations", str(info.statistics.total_unresolved_relations)
|
|
58
|
+
)
|
|
59
|
+
stats_table.add_row("Isolated Entities", str(info.statistics.isolated_entities))
|
|
60
|
+
|
|
61
|
+
console.print(stats_table)
|
|
62
|
+
|
|
63
|
+
# Entity types
|
|
64
|
+
if info.statistics.entity_types:
|
|
65
|
+
entity_types_table = Table(title="📑 Entity Types")
|
|
66
|
+
entity_types_table.add_column("Type", style="blue")
|
|
67
|
+
entity_types_table.add_column("Count", style="green")
|
|
68
|
+
|
|
69
|
+
for entity_type, count in info.statistics.entity_types.items():
|
|
70
|
+
entity_types_table.add_row(entity_type, str(count))
|
|
71
|
+
|
|
72
|
+
console.print(entity_types_table)
|
|
73
|
+
|
|
74
|
+
# Most connected entities
|
|
75
|
+
if info.statistics.most_connected_entities:
|
|
76
|
+
connected_table = Table(title="🔗 Most Connected Entities")
|
|
77
|
+
connected_table.add_column("Title", style="blue")
|
|
78
|
+
connected_table.add_column("Permalink", style="cyan")
|
|
79
|
+
connected_table.add_column("Relations", style="green")
|
|
80
|
+
|
|
81
|
+
for entity in info.statistics.most_connected_entities:
|
|
82
|
+
connected_table.add_row(
|
|
83
|
+
entity["title"], entity["permalink"], str(entity["relation_count"])
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
console.print(connected_table)
|
|
87
|
+
|
|
88
|
+
# Recent activity
|
|
89
|
+
if info.activity.recently_updated:
|
|
90
|
+
recent_table = Table(title="🕒 Recent Activity")
|
|
91
|
+
recent_table.add_column("Title", style="blue")
|
|
92
|
+
recent_table.add_column("Type", style="cyan")
|
|
93
|
+
recent_table.add_column("Last Updated", style="green")
|
|
94
|
+
|
|
95
|
+
for entity in info.activity.recently_updated[:5]: # Show top 5
|
|
96
|
+
updated_at = (
|
|
97
|
+
datetime.fromisoformat(entity["updated_at"])
|
|
98
|
+
if isinstance(entity["updated_at"], str)
|
|
99
|
+
else entity["updated_at"]
|
|
100
|
+
)
|
|
101
|
+
recent_table.add_row(
|
|
102
|
+
entity["title"],
|
|
103
|
+
entity["entity_type"],
|
|
104
|
+
updated_at.strftime("%Y-%m-%d %H:%M"),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
console.print(recent_table)
|
|
108
|
+
|
|
109
|
+
# System status
|
|
110
|
+
system_tree = Tree("🖥️ System Status")
|
|
111
|
+
system_tree.add(f"Basic Memory version: [bold green]{info.system.version}[/bold green]")
|
|
112
|
+
system_tree.add(
|
|
113
|
+
f"Database: [cyan]{info.system.database_path}[/cyan] ([green]{info.system.database_size}[/green])"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Watch status
|
|
117
|
+
if info.system.watch_status: # pragma: no cover
|
|
118
|
+
watch_branch = system_tree.add("Watch Service")
|
|
119
|
+
running = info.system.watch_status.get("running", False)
|
|
120
|
+
status_color = "green" if running else "red"
|
|
121
|
+
watch_branch.add(
|
|
122
|
+
f"Status: [bold {status_color}]{'Running' if running else 'Stopped'}[/bold {status_color}]"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if running:
|
|
126
|
+
start_time = (
|
|
127
|
+
datetime.fromisoformat(info.system.watch_status.get("start_time", ""))
|
|
128
|
+
if isinstance(info.system.watch_status.get("start_time"), str)
|
|
129
|
+
else info.system.watch_status.get("start_time")
|
|
130
|
+
)
|
|
131
|
+
watch_branch.add(
|
|
132
|
+
f"Running since: [cyan]{start_time.strftime('%Y-%m-%d %H:%M')}[/cyan]"
|
|
133
|
+
)
|
|
134
|
+
watch_branch.add(
|
|
135
|
+
f"Files synced: [green]{info.system.watch_status.get('synced_files', 0)}[/green]"
|
|
136
|
+
)
|
|
137
|
+
watch_branch.add(
|
|
138
|
+
f"Errors: [{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]{info.system.watch_status.get('error_count', 0)}[/{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]"
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
system_tree.add("[yellow]Watch service not running[/yellow]")
|
|
142
|
+
|
|
143
|
+
console.print(system_tree)
|
|
144
|
+
|
|
145
|
+
# Available projects
|
|
146
|
+
projects_table = Table(title="📁 Available Projects")
|
|
147
|
+
projects_table.add_column("Name", style="blue")
|
|
148
|
+
projects_table.add_column("Path", style="cyan")
|
|
149
|
+
projects_table.add_column("Default", style="green")
|
|
150
|
+
|
|
151
|
+
for name, path in info.available_projects.items():
|
|
152
|
+
is_default = name == info.default_project
|
|
153
|
+
projects_table.add_row(name, path, "✓" if is_default else "")
|
|
154
|
+
|
|
155
|
+
console.print(projects_table)
|
|
156
|
+
|
|
157
|
+
# Timestamp
|
|
158
|
+
current_time = (
|
|
159
|
+
datetime.fromisoformat(str(info.system.timestamp))
|
|
160
|
+
if isinstance(info.system.timestamp, str)
|
|
161
|
+
else info.system.timestamp
|
|
162
|
+
)
|
|
163
|
+
console.print(f"\nTimestamp: [cyan]{current_time.strftime('%Y-%m-%d %H:%M:%S')}[/cyan]")
|
|
164
|
+
|
|
165
|
+
except Exception as e: # pragma: no cover
|
|
166
|
+
typer.echo(f"Error getting project info: {e}", err=True)
|
|
167
|
+
raise typer.Exit(1)
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from typing import Set, Dict
|
|
5
5
|
|
|
6
|
-
import logfire
|
|
7
6
|
import typer
|
|
8
7
|
from loguru import logger
|
|
9
8
|
from rich.console import Console
|
|
@@ -134,11 +133,10 @@ def status(
|
|
|
134
133
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed file information"),
|
|
135
134
|
):
|
|
136
135
|
"""Show sync status between files and database."""
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
raise typer.Exit(code=1) # pragma: no cover
|
|
136
|
+
try:
|
|
137
|
+
sync_service = asyncio.run(get_sync_service())
|
|
138
|
+
asyncio.run(run_status(sync_service, verbose)) # pragma: no cover
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.exception(f"Error checking status: {e}")
|
|
141
|
+
typer.echo(f"Error checking status: {e}", err=True)
|
|
142
|
+
raise typer.Exit(code=1) # pragma: no cover
|
|
@@ -93,8 +93,10 @@ def group_issues_by_directory(issues: List[ValidationIssue]) -> Dict[str, List[V
|
|
|
93
93
|
def display_sync_summary(knowledge: SyncReport):
|
|
94
94
|
"""Display a one-line summary of sync changes."""
|
|
95
95
|
total_changes = knowledge.total
|
|
96
|
+
project_name = config.project
|
|
97
|
+
|
|
96
98
|
if total_changes == 0:
|
|
97
|
-
console.print("[green]Everything up to date[/green]")
|
|
99
|
+
console.print(f"[green]Project '{project_name}': Everything up to date[/green]")
|
|
98
100
|
return
|
|
99
101
|
|
|
100
102
|
# Format as: "Synced X files (A new, B modified, C moved, D deleted)"
|
|
@@ -113,16 +115,18 @@ def display_sync_summary(knowledge: SyncReport):
|
|
|
113
115
|
if del_count:
|
|
114
116
|
changes.append(f"[red]{del_count} deleted[/red]")
|
|
115
117
|
|
|
116
|
-
console.print(f"Synced {total_changes} files ({', '.join(changes)})")
|
|
118
|
+
console.print(f"Project '{project_name}': Synced {total_changes} files ({', '.join(changes)})")
|
|
117
119
|
|
|
118
120
|
|
|
119
121
|
def display_detailed_sync_results(knowledge: SyncReport):
|
|
120
122
|
"""Display detailed sync results with trees."""
|
|
123
|
+
project_name = config.project
|
|
124
|
+
|
|
121
125
|
if knowledge.total == 0:
|
|
122
|
-
console.print("\n[green]Everything up to date[/green]")
|
|
126
|
+
console.print(f"\n[green]Project '{project_name}': Everything up to date[/green]")
|
|
123
127
|
return
|
|
124
128
|
|
|
125
|
-
console.print("\n[bold]Sync Results[/bold]")
|
|
129
|
+
console.print(f"\n[bold]Sync Results for Project '{project_name}'[/bold]")
|
|
126
130
|
|
|
127
131
|
if knowledge.total > 0:
|
|
128
132
|
knowledge_tree = Tree("[bold]Knowledge Files[/bold]")
|
|
@@ -150,23 +154,52 @@ def display_detailed_sync_results(knowledge: SyncReport):
|
|
|
150
154
|
|
|
151
155
|
async def run_sync(verbose: bool = False, watch: bool = False, console_status: bool = False):
|
|
152
156
|
"""Run sync operation."""
|
|
157
|
+
import time
|
|
158
|
+
|
|
159
|
+
start_time = time.time()
|
|
160
|
+
|
|
161
|
+
logger.info(
|
|
162
|
+
"Sync command started",
|
|
163
|
+
project=config.project,
|
|
164
|
+
watch_mode=watch,
|
|
165
|
+
verbose=verbose,
|
|
166
|
+
directory=str(config.home),
|
|
167
|
+
)
|
|
153
168
|
|
|
154
169
|
sync_service = await get_sync_service()
|
|
155
170
|
|
|
156
171
|
# Start watching if requested
|
|
157
172
|
if watch:
|
|
173
|
+
logger.info("Starting watch service after initial sync")
|
|
158
174
|
watch_service = WatchService(
|
|
159
175
|
sync_service=sync_service,
|
|
160
176
|
file_service=sync_service.entity_service.file_service,
|
|
161
177
|
config=config,
|
|
162
178
|
)
|
|
163
|
-
|
|
164
|
-
|
|
179
|
+
|
|
180
|
+
# full sync - no progress bars in watch mode
|
|
181
|
+
await sync_service.sync(config.home, show_progress=False)
|
|
182
|
+
|
|
165
183
|
# watch changes
|
|
166
184
|
await watch_service.run() # pragma: no cover
|
|
167
185
|
else:
|
|
168
|
-
# one time sync
|
|
169
|
-
|
|
186
|
+
# one time sync - use progress bars for better UX
|
|
187
|
+
logger.info("Running one-time sync")
|
|
188
|
+
knowledge_changes = await sync_service.sync(config.home, show_progress=True)
|
|
189
|
+
|
|
190
|
+
# Log results
|
|
191
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
192
|
+
logger.info(
|
|
193
|
+
"Sync command completed",
|
|
194
|
+
project=config.project,
|
|
195
|
+
total_changes=knowledge_changes.total,
|
|
196
|
+
new_files=len(knowledge_changes.new),
|
|
197
|
+
modified_files=len(knowledge_changes.modified),
|
|
198
|
+
deleted_files=len(knowledge_changes.deleted),
|
|
199
|
+
moved_files=len(knowledge_changes.moves),
|
|
200
|
+
duration_ms=duration_ms,
|
|
201
|
+
)
|
|
202
|
+
|
|
170
203
|
# Display results
|
|
171
204
|
if verbose:
|
|
172
205
|
display_detailed_sync_results(knowledge_changes)
|
|
@@ -191,12 +224,24 @@ def sync(
|
|
|
191
224
|
) -> None:
|
|
192
225
|
"""Sync knowledge files with the database."""
|
|
193
226
|
try:
|
|
227
|
+
# Show which project we're syncing
|
|
228
|
+
if not watch: # Don't show in watch mode as it would break the UI
|
|
229
|
+
typer.echo(f"Syncing project: {config.project}")
|
|
230
|
+
typer.echo(f"Project path: {config.home}")
|
|
231
|
+
|
|
194
232
|
# Run sync
|
|
195
233
|
asyncio.run(run_sync(verbose=verbose, watch=watch))
|
|
196
234
|
|
|
197
235
|
except Exception as e: # pragma: no cover
|
|
198
236
|
if not isinstance(e, typer.Exit):
|
|
199
|
-
logger.exception(
|
|
237
|
+
logger.exception(
|
|
238
|
+
"Sync command failed",
|
|
239
|
+
project=config.project,
|
|
240
|
+
error=str(e),
|
|
241
|
+
error_type=type(e).__name__,
|
|
242
|
+
watch_mode=watch,
|
|
243
|
+
directory=str(config.home),
|
|
244
|
+
)
|
|
200
245
|
typer.echo(f"Error during sync: {e}", err=True)
|
|
201
246
|
raise typer.Exit(1)
|
|
202
247
|
raise
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""CLI tool commands for Basic Memory."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import sys
|
|
4
5
|
from typing import Optional, List, Annotated
|
|
5
6
|
|
|
6
7
|
import typer
|
|
@@ -9,7 +10,6 @@ from rich import print as rprint
|
|
|
9
10
|
|
|
10
11
|
from basic_memory.cli.app import app
|
|
11
12
|
from basic_memory.mcp.tools import build_context as mcp_build_context
|
|
12
|
-
from basic_memory.mcp.tools import get_entity as mcp_get_entity
|
|
13
13
|
from basic_memory.mcp.tools import read_note as mcp_read_note
|
|
14
14
|
from basic_memory.mcp.tools import recent_activity as mcp_recent_activity
|
|
15
15
|
from basic_memory.mcp.tools import search as mcp_search
|
|
@@ -20,24 +20,78 @@ from basic_memory.mcp.prompts.continue_conversation import (
|
|
|
20
20
|
continue_conversation as mcp_continue_conversation,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
+
from basic_memory.mcp.prompts.recent_activity import (
|
|
24
|
+
recent_activity_prompt as recent_activity_prompt,
|
|
25
|
+
)
|
|
26
|
+
|
|
23
27
|
from basic_memory.schemas.base import TimeFrame
|
|
24
28
|
from basic_memory.schemas.memory import MemoryUrl
|
|
25
29
|
from basic_memory.schemas.search import SearchQuery, SearchItemType
|
|
26
30
|
|
|
27
31
|
tool_app = typer.Typer()
|
|
28
|
-
app.add_typer(tool_app, name="
|
|
32
|
+
app.add_typer(tool_app, name="tool", help="Direct access to MCP tools via CLI")
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
@tool_app.command()
|
|
32
36
|
def write_note(
|
|
33
37
|
title: Annotated[str, typer.Option(help="The title of the note")],
|
|
34
|
-
content: Annotated[str, typer.Option(help="The content of the note")],
|
|
35
38
|
folder: Annotated[str, typer.Option(help="The folder to create the note in")],
|
|
39
|
+
content: Annotated[
|
|
40
|
+
Optional[str],
|
|
41
|
+
typer.Option(
|
|
42
|
+
help="The content of the note. If not provided, content will be read from stdin. This allows piping content from other commands, e.g.: cat file.md | basic-memory tools write-note"
|
|
43
|
+
),
|
|
44
|
+
] = None,
|
|
36
45
|
tags: Annotated[
|
|
37
46
|
Optional[List[str]], typer.Option(help="A list of tags to apply to the note")
|
|
38
47
|
] = None,
|
|
39
48
|
):
|
|
49
|
+
"""Create or update a markdown note. Content can be provided as an argument or read from stdin.
|
|
50
|
+
|
|
51
|
+
Content can be provided in two ways:
|
|
52
|
+
1. Using the --content parameter
|
|
53
|
+
2. Piping content through stdin (if --content is not provided)
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
|
|
57
|
+
# Using content parameter
|
|
58
|
+
basic-memory tools write-note --title "My Note" --folder "notes" --content "Note content"
|
|
59
|
+
|
|
60
|
+
# Using stdin pipe
|
|
61
|
+
echo "# My Note Content" | basic-memory tools write-note --title "My Note" --folder "notes"
|
|
62
|
+
|
|
63
|
+
# Using heredoc
|
|
64
|
+
cat << EOF | basic-memory tools write-note --title "My Note" --folder "notes"
|
|
65
|
+
# My Document
|
|
66
|
+
|
|
67
|
+
This is my document content.
|
|
68
|
+
|
|
69
|
+
- Point 1
|
|
70
|
+
- Point 2
|
|
71
|
+
EOF
|
|
72
|
+
|
|
73
|
+
# Reading from a file
|
|
74
|
+
cat document.md | basic-memory tools write-note --title "Document" --folder "docs"
|
|
75
|
+
"""
|
|
40
76
|
try:
|
|
77
|
+
# If content is not provided, read from stdin
|
|
78
|
+
if content is None:
|
|
79
|
+
# Check if we're getting data from a pipe or redirect
|
|
80
|
+
if not sys.stdin.isatty():
|
|
81
|
+
content = sys.stdin.read()
|
|
82
|
+
else: # pragma: no cover
|
|
83
|
+
# If stdin is a terminal (no pipe/redirect), inform the user
|
|
84
|
+
typer.echo(
|
|
85
|
+
"No content provided. Please provide content via --content or by piping to stdin.",
|
|
86
|
+
err=True,
|
|
87
|
+
)
|
|
88
|
+
raise typer.Exit(1)
|
|
89
|
+
|
|
90
|
+
# Also check for empty content
|
|
91
|
+
if content is not None and not content.strip():
|
|
92
|
+
typer.echo("Empty content provided. Please provide non-empty content.", err=True)
|
|
93
|
+
raise typer.Exit(1)
|
|
94
|
+
|
|
41
95
|
note = asyncio.run(mcp_write_note(title, content, folder, tags))
|
|
42
96
|
rprint(note)
|
|
43
97
|
except Exception as e: # pragma: no cover
|
|
@@ -79,7 +133,11 @@ def build_context(
|
|
|
79
133
|
max_related=max_related,
|
|
80
134
|
)
|
|
81
135
|
)
|
|
82
|
-
|
|
136
|
+
# Use json module for more controlled serialization
|
|
137
|
+
import json
|
|
138
|
+
|
|
139
|
+
context_dict = context.model_dump(exclude_none=True)
|
|
140
|
+
print(json.dumps(context_dict, indent=2, ensure_ascii=True, default=str))
|
|
83
141
|
except Exception as e: # pragma: no cover
|
|
84
142
|
if not isinstance(e, typer.Exit):
|
|
85
143
|
typer.echo(f"Error during build_context: {e}", err=True)
|
|
@@ -107,7 +165,11 @@ def recent_activity(
|
|
|
107
165
|
max_related=max_related,
|
|
108
166
|
)
|
|
109
167
|
)
|
|
110
|
-
|
|
168
|
+
# Use json module for more controlled serialization
|
|
169
|
+
import json
|
|
170
|
+
|
|
171
|
+
context_dict = context.model_dump(exclude_none=True)
|
|
172
|
+
print(json.dumps(context_dict, indent=2, ensure_ascii=True, default=str))
|
|
111
173
|
except Exception as e: # pragma: no cover
|
|
112
174
|
if not isinstance(e, typer.Exit):
|
|
113
175
|
typer.echo(f"Error during build_context: {e}", err=True)
|
|
@@ -139,7 +201,11 @@ def search(
|
|
|
139
201
|
after_date=after_date,
|
|
140
202
|
)
|
|
141
203
|
results = asyncio.run(mcp_search(query=search_query, page=page, page_size=page_size))
|
|
142
|
-
|
|
204
|
+
# Use json module for more controlled serialization
|
|
205
|
+
import json
|
|
206
|
+
|
|
207
|
+
results_dict = results.model_dump(exclude_none=True)
|
|
208
|
+
print(json.dumps(results_dict, indent=2, ensure_ascii=True, default=str))
|
|
143
209
|
except Exception as e: # pragma: no cover
|
|
144
210
|
if not isinstance(e, typer.Exit):
|
|
145
211
|
logger.exception("Error during search", e)
|
|
@@ -148,18 +214,6 @@ def search(
|
|
|
148
214
|
raise
|
|
149
215
|
|
|
150
216
|
|
|
151
|
-
@tool_app.command()
|
|
152
|
-
def get_entity(identifier: str):
|
|
153
|
-
try:
|
|
154
|
-
entity = asyncio.run(mcp_get_entity(identifier=identifier))
|
|
155
|
-
rprint(entity.model_dump_json(indent=2))
|
|
156
|
-
except Exception as e: # pragma: no cover
|
|
157
|
-
if not isinstance(e, typer.Exit):
|
|
158
|
-
typer.echo(f"Error during get_entity: {e}", err=True)
|
|
159
|
-
raise typer.Exit(1)
|
|
160
|
-
raise
|
|
161
|
-
|
|
162
|
-
|
|
163
217
|
@tool_app.command(name="continue-conversation")
|
|
164
218
|
def continue_conversation(
|
|
165
219
|
topic: Annotated[Optional[str], typer.Option(help="Topic or keyword to search for")] = None,
|
|
@@ -167,7 +221,7 @@ def continue_conversation(
|
|
|
167
221
|
Optional[str], typer.Option(help="How far back to look for activity")
|
|
168
222
|
] = None,
|
|
169
223
|
):
|
|
170
|
-
"""
|
|
224
|
+
"""Prompt to continue a previous conversation or work session."""
|
|
171
225
|
try:
|
|
172
226
|
# Prompt functions return formatted strings directly
|
|
173
227
|
session = asyncio.run(mcp_continue_conversation(topic=topic, timeframe=timeframe))
|
|
@@ -178,3 +232,22 @@ def continue_conversation(
|
|
|
178
232
|
typer.echo(f"Error continuing conversation: {e}", err=True)
|
|
179
233
|
raise typer.Exit(1)
|
|
180
234
|
raise
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# @tool_app.command(name="show-recent-activity")
|
|
238
|
+
# def show_recent_activity(
|
|
239
|
+
# timeframe: Annotated[
|
|
240
|
+
# str, typer.Option(help="How far back to look for activity")
|
|
241
|
+
# ] = "7d",
|
|
242
|
+
# ):
|
|
243
|
+
# """Prompt to show recent activity."""
|
|
244
|
+
# try:
|
|
245
|
+
# # Prompt functions return formatted strings directly
|
|
246
|
+
# session = asyncio.run(recent_activity_prompt(timeframe=timeframe))
|
|
247
|
+
# rprint(session)
|
|
248
|
+
# except Exception as e: # pragma: no cover
|
|
249
|
+
# if not isinstance(e, typer.Exit):
|
|
250
|
+
# logger.exception("Error continuing conversation", e)
|
|
251
|
+
# typer.echo(f"Error continuing conversation: {e}", err=True)
|
|
252
|
+
# raise typer.Exit(1)
|
|
253
|
+
# raise
|
basic_memory/cli/main.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Main CLI entry point for basic-memory.""" # pragma: no cover
|
|
2
2
|
|
|
3
3
|
from basic_memory.cli.app import app # pragma: no cover
|
|
4
|
+
import typer
|
|
4
5
|
|
|
5
6
|
# Register commands
|
|
6
7
|
from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
|
|
@@ -12,8 +13,46 @@ from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
|
|
|
12
13
|
import_claude_conversations,
|
|
13
14
|
import_claude_projects,
|
|
14
15
|
import_chatgpt,
|
|
15
|
-
|
|
16
|
+
tool,
|
|
17
|
+
project,
|
|
16
18
|
)
|
|
17
19
|
|
|
20
|
+
|
|
21
|
+
# Version command
|
|
22
|
+
@app.callback(invoke_without_command=True)
|
|
23
|
+
def main(
|
|
24
|
+
ctx: typer.Context,
|
|
25
|
+
project: str = typer.Option( # noqa
|
|
26
|
+
"main",
|
|
27
|
+
"--project",
|
|
28
|
+
"-p",
|
|
29
|
+
help="Specify which project to use",
|
|
30
|
+
envvar="BASIC_MEMORY_PROJECT",
|
|
31
|
+
),
|
|
32
|
+
version: bool = typer.Option(
|
|
33
|
+
False,
|
|
34
|
+
"--version",
|
|
35
|
+
"-V",
|
|
36
|
+
help="Show version information and exit.",
|
|
37
|
+
is_eager=True,
|
|
38
|
+
),
|
|
39
|
+
):
|
|
40
|
+
"""Basic Memory - Local-first personal knowledge management system."""
|
|
41
|
+
if version: # pragma: no cover
|
|
42
|
+
from basic_memory import __version__
|
|
43
|
+
from basic_memory.config import config
|
|
44
|
+
|
|
45
|
+
typer.echo(f"Basic Memory v{__version__}")
|
|
46
|
+
typer.echo(f"Current project: {config.project}")
|
|
47
|
+
typer.echo(f"Project path: {config.home}")
|
|
48
|
+
raise typer.Exit()
|
|
49
|
+
|
|
50
|
+
# Handle project selection via environment variable
|
|
51
|
+
if project:
|
|
52
|
+
import os
|
|
53
|
+
|
|
54
|
+
os.environ["BASIC_MEMORY_PROJECT"] = project
|
|
55
|
+
|
|
56
|
+
|
|
18
57
|
if __name__ == "__main__": # pragma: no cover
|
|
19
58
|
app()
|