basic-memory 0.8.0__py3-none-any.whl → 0.9.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 +275 -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 +26 -30
- basic_memory/cli/commands/import_claude_conversations.py +27 -29
- basic_memory/cli/commands/import_claude_projects.py +29 -31
- 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 +155 -7
- basic_memory/db.py +19 -4
- basic_memory/deps.py +10 -3
- basic_memory/file_utils.py +32 -16
- 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 +6 -8
- 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 +105 -53
- 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 +160 -30
- basic_memory/utils.py +40 -40
- basic_memory-0.9.0.dist-info/METADATA +736 -0
- basic_memory-0.9.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.9.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.8.0.dist-info → basic_memory-0.9.0.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.8.0.dist-info → basic_memory-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
"""CLI commands for basic-memory."""
|
|
2
2
|
|
|
3
|
-
from . import status, sync, db, import_memory_json, mcp
|
|
3
|
+
from . import status, sync, db, import_memory_json, mcp, import_claude_conversations
|
|
4
|
+
from . import import_claude_projects, import_chatgpt, tool, project, project_info
|
|
4
5
|
|
|
5
|
-
__all__ = [
|
|
6
|
+
__all__ = [
|
|
7
|
+
"status",
|
|
8
|
+
"sync",
|
|
9
|
+
"db",
|
|
10
|
+
"import_memory_json",
|
|
11
|
+
"mcp",
|
|
12
|
+
"import_claude_conversations",
|
|
13
|
+
"import_claude_projects",
|
|
14
|
+
"import_chatgpt",
|
|
15
|
+
"tool",
|
|
16
|
+
"project",
|
|
17
|
+
"project_info",
|
|
18
|
+
]
|
basic_memory/cli/commands/db.py
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
"""Database management commands."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
|
-
|
|
5
|
-
import logfire
|
|
6
3
|
import typer
|
|
7
4
|
from loguru import logger
|
|
8
5
|
|
|
@@ -12,17 +9,16 @@ from basic_memory.cli.app import app
|
|
|
12
9
|
|
|
13
10
|
@app.command()
|
|
14
11
|
def reset(
|
|
15
|
-
reindex: bool = typer.Option(False, "--reindex", help="Rebuild
|
|
12
|
+
reindex: bool = typer.Option(False, "--reindex", help="Rebuild db index from filesystem"),
|
|
16
13
|
): # pragma: no cover
|
|
17
14
|
"""Reset database (drop all tables and recreate)."""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
asyncio.run(migrations.reset_database())
|
|
15
|
+
if typer.confirm("This will delete all data in your db. Are you sure?"):
|
|
16
|
+
logger.info("Resetting database...")
|
|
17
|
+
migrations.reset_database()
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
if reindex:
|
|
20
|
+
# Import and run sync
|
|
21
|
+
from basic_memory.cli.commands.sync import sync
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
logger.info("Rebuilding search index from filesystem...")
|
|
24
|
+
sync(watch=False) # pyright: ignore
|
|
@@ -6,7 +6,6 @@ from datetime import datetime
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Dict, Any, List, Annotated, Set, Optional
|
|
8
8
|
|
|
9
|
-
import logfire
|
|
10
9
|
import typer
|
|
11
10
|
from loguru import logger
|
|
12
11
|
from rich.console import Console
|
|
@@ -226,38 +225,35 @@ def import_chatgpt(
|
|
|
226
225
|
After importing, run 'basic-memory sync' to index the new files.
|
|
227
226
|
"""
|
|
228
227
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if conversations_json:
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
raise typer.Exit(1)
|
|
228
|
+
try:
|
|
229
|
+
if conversations_json:
|
|
230
|
+
if not conversations_json.exists():
|
|
231
|
+
typer.echo(f"Error: File not found: {conversations_json}", err=True)
|
|
232
|
+
raise typer.Exit(1)
|
|
235
233
|
|
|
236
|
-
|
|
237
|
-
|
|
234
|
+
# Get markdown processor
|
|
235
|
+
markdown_processor = asyncio.run(get_markdown_processor())
|
|
238
236
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
process_chatgpt_json(conversations_json, folder, markdown_processor)
|
|
246
|
-
)
|
|
237
|
+
# Process the file
|
|
238
|
+
base_path = config.home / folder
|
|
239
|
+
console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
|
|
240
|
+
results = asyncio.run(
|
|
241
|
+
process_chatgpt_json(conversations_json, folder, markdown_processor)
|
|
242
|
+
)
|
|
247
243
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
)
|
|
244
|
+
# Show results
|
|
245
|
+
console.print(
|
|
246
|
+
Panel(
|
|
247
|
+
f"[green]Import complete![/green]\n\n"
|
|
248
|
+
f"Imported {results['conversations']} conversations\n"
|
|
249
|
+
f"Containing {results['messages']} messages",
|
|
250
|
+
expand=False,
|
|
256
251
|
)
|
|
252
|
+
)
|
|
257
253
|
|
|
258
|
-
|
|
254
|
+
console.print("\nRun 'basic-memory sync' to index the new files.")
|
|
259
255
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
256
|
+
except Exception as e:
|
|
257
|
+
logger.error("Import failed")
|
|
258
|
+
typer.echo(f"Error during import: {e}", err=True)
|
|
259
|
+
raise typer.Exit(1)
|
|
@@ -6,7 +6,6 @@ from datetime import datetime
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Dict, Any, List, Annotated
|
|
8
8
|
|
|
9
|
-
import logfire
|
|
10
9
|
import typer
|
|
11
10
|
from loguru import logger
|
|
12
11
|
from rich.console import Console
|
|
@@ -179,35 +178,34 @@ def import_claude(
|
|
|
179
178
|
After importing, run 'basic-memory sync' to index the new files.
|
|
180
179
|
"""
|
|
181
180
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
raise typer.Exit(1)
|
|
187
|
-
|
|
188
|
-
# Get markdown processor
|
|
189
|
-
markdown_processor = asyncio.run(get_markdown_processor())
|
|
190
|
-
|
|
191
|
-
# Process the file
|
|
192
|
-
base_path = config.home / folder
|
|
193
|
-
console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
|
|
194
|
-
results = asyncio.run(
|
|
195
|
-
process_conversations_json(conversations_json, base_path, markdown_processor)
|
|
196
|
-
)
|
|
181
|
+
try:
|
|
182
|
+
if not conversations_json.exists():
|
|
183
|
+
typer.echo(f"Error: File not found: {conversations_json}", err=True)
|
|
184
|
+
raise typer.Exit(1)
|
|
197
185
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
186
|
+
# Get markdown processor
|
|
187
|
+
markdown_processor = asyncio.run(get_markdown_processor())
|
|
188
|
+
|
|
189
|
+
# Process the file
|
|
190
|
+
base_path = config.home / folder
|
|
191
|
+
console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
|
|
192
|
+
results = asyncio.run(
|
|
193
|
+
process_conversations_json(conversations_json, base_path, markdown_processor)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Show results
|
|
197
|
+
console.print(
|
|
198
|
+
Panel(
|
|
199
|
+
f"[green]Import complete![/green]\n\n"
|
|
200
|
+
f"Imported {results['conversations']} conversations\n"
|
|
201
|
+
f"Containing {results['messages']} messages",
|
|
202
|
+
expand=False,
|
|
206
203
|
)
|
|
204
|
+
)
|
|
207
205
|
|
|
208
|
-
|
|
206
|
+
console.print("\nRun 'basic-memory sync' to index the new files.")
|
|
209
207
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.error("Import failed")
|
|
210
|
+
typer.echo(f"Error during import: {e}", err=True)
|
|
211
|
+
raise typer.Exit(1)
|
|
@@ -5,7 +5,6 @@ import json
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Dict, Any, Annotated, Optional
|
|
7
7
|
|
|
8
|
-
import logfire
|
|
9
8
|
import typer
|
|
10
9
|
from loguru import logger
|
|
11
10
|
from rich.console import Console
|
|
@@ -161,36 +160,35 @@ def import_projects(
|
|
|
161
160
|
|
|
162
161
|
After importing, run 'basic-memory sync' to index the new files.
|
|
163
162
|
"""
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if projects_json:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
163
|
+
try:
|
|
164
|
+
if projects_json:
|
|
165
|
+
if not projects_json.exists():
|
|
166
|
+
typer.echo(f"Error: File not found: {projects_json}", err=True)
|
|
167
|
+
raise typer.Exit(1)
|
|
168
|
+
|
|
169
|
+
# Get markdown processor
|
|
170
|
+
markdown_processor = asyncio.run(get_markdown_processor())
|
|
171
|
+
|
|
172
|
+
# Process the file
|
|
173
|
+
base_path = config.home / base_folder if base_folder else config.home
|
|
174
|
+
console.print(f"\nImporting projects from {projects_json}...writing to {base_path}")
|
|
175
|
+
results = asyncio.run(
|
|
176
|
+
process_projects_json(projects_json, base_path, markdown_processor)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Show results
|
|
180
|
+
console.print(
|
|
181
|
+
Panel(
|
|
182
|
+
f"[green]Import complete![/green]\n\n"
|
|
183
|
+
f"Imported {results['documents']} project documents\n"
|
|
184
|
+
f"Imported {results['prompts']} prompt templates",
|
|
185
|
+
expand=False,
|
|
179
186
|
)
|
|
187
|
+
)
|
|
180
188
|
|
|
181
|
-
|
|
182
|
-
console.print(
|
|
183
|
-
Panel(
|
|
184
|
-
f"[green]Import complete![/green]\n\n"
|
|
185
|
-
f"Imported {results['documents']} project documents\n"
|
|
186
|
-
f"Imported {results['prompts']} prompt templates",
|
|
187
|
-
expand=False,
|
|
188
|
-
)
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
console.print("\nRun 'basic-memory sync' to index the new files.")
|
|
189
|
+
console.print("\nRun 'basic-memory sync' to index the new files.")
|
|
192
190
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.error("Import failed")
|
|
193
|
+
typer.echo(f"Error during import: {e}", err=True)
|
|
194
|
+
raise typer.Exit(1)
|
|
@@ -5,7 +5,6 @@ import json
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Dict, Any, List, Annotated
|
|
7
7
|
|
|
8
|
-
import logfire
|
|
9
8
|
import typer
|
|
10
9
|
from loguru import logger
|
|
11
10
|
from rich.console import Console
|
|
@@ -114,33 +113,32 @@ def memory_json(
|
|
|
114
113
|
After importing, run 'basic-memory sync' to index the new files.
|
|
115
114
|
"""
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
expand=False,
|
|
138
|
-
)
|
|
116
|
+
if not json_path.exists():
|
|
117
|
+
typer.echo(f"Error: File not found: {json_path}", err=True)
|
|
118
|
+
raise typer.Exit(1)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
# Get markdown processor
|
|
122
|
+
markdown_processor = asyncio.run(get_markdown_processor())
|
|
123
|
+
|
|
124
|
+
# Process the file
|
|
125
|
+
base_path = config.home
|
|
126
|
+
console.print(f"\nImporting from {json_path}...writing to {base_path}")
|
|
127
|
+
results = asyncio.run(process_memory_json(json_path, base_path, markdown_processor))
|
|
128
|
+
|
|
129
|
+
# Show results
|
|
130
|
+
console.print(
|
|
131
|
+
Panel(
|
|
132
|
+
f"[green]Import complete![/green]\n\n"
|
|
133
|
+
f"Created {results['entities']} entities\n"
|
|
134
|
+
f"Added {results['relations']} relations",
|
|
135
|
+
expand=False,
|
|
139
136
|
)
|
|
137
|
+
)
|
|
140
138
|
|
|
141
|
-
|
|
139
|
+
console.print("\nRun 'basic-memory sync' to index the new files.")
|
|
142
140
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error("Import failed")
|
|
143
|
+
typer.echo(f"Error during import: {e}", err=True)
|
|
144
|
+
raise typer.Exit(1)
|
basic_memory/cli/commands/mcp.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""MCP server command."""
|
|
2
2
|
|
|
3
3
|
from loguru import logger
|
|
4
|
+
|
|
5
|
+
import basic_memory
|
|
4
6
|
from basic_memory.cli.app import app
|
|
5
7
|
from basic_memory.config import config
|
|
6
8
|
|
|
@@ -15,6 +17,10 @@ import basic_memory.mcp.tools # noqa: F401 # pragma: no cover
|
|
|
15
17
|
def mcp(): # pragma: no cover
|
|
16
18
|
"""Run the MCP server for Claude Desktop integration."""
|
|
17
19
|
home_dir = config.home
|
|
20
|
+
project_name = config.project
|
|
21
|
+
|
|
18
22
|
logger.info(f"Starting Basic Memory MCP server {basic_memory.__version__}")
|
|
19
|
-
logger.info(f"
|
|
23
|
+
logger.info(f"Project: {project_name}")
|
|
24
|
+
logger.info(f"Project directory: {home_dir}")
|
|
25
|
+
|
|
20
26
|
mcp_server.run()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Command module for basic-memory project management."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from basic_memory.cli.app import app
|
|
11
|
+
from basic_memory.config import ConfigManager, config
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
# Create a project subcommand
|
|
16
|
+
project_app = typer.Typer(help="Manage multiple Basic Memory projects")
|
|
17
|
+
app.add_typer(project_app, name="project")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def format_path(path: str) -> str:
|
|
21
|
+
"""Format a path for display, using ~ for home directory."""
|
|
22
|
+
home = str(Path.home())
|
|
23
|
+
if path.startswith(home):
|
|
24
|
+
return path.replace(home, "~", 1)
|
|
25
|
+
return path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@project_app.command("list")
|
|
29
|
+
def list_projects() -> None:
|
|
30
|
+
"""List all configured projects."""
|
|
31
|
+
config_manager = ConfigManager()
|
|
32
|
+
projects = config_manager.projects
|
|
33
|
+
|
|
34
|
+
table = Table(title="Basic Memory Projects")
|
|
35
|
+
table.add_column("Name", style="cyan")
|
|
36
|
+
table.add_column("Path", style="green")
|
|
37
|
+
table.add_column("Default", style="yellow")
|
|
38
|
+
table.add_column("Active", style="magenta")
|
|
39
|
+
|
|
40
|
+
default_project = config_manager.default_project
|
|
41
|
+
active_project = config.project
|
|
42
|
+
|
|
43
|
+
for name, path in projects.items():
|
|
44
|
+
is_default = "✓" if name == default_project else ""
|
|
45
|
+
is_active = "✓" if name == active_project else ""
|
|
46
|
+
table.add_row(name, format_path(path), is_default, is_active)
|
|
47
|
+
|
|
48
|
+
console.print(table)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@project_app.command("add")
|
|
52
|
+
def add_project(
|
|
53
|
+
name: str = typer.Argument(..., help="Name of the project"),
|
|
54
|
+
path: str = typer.Argument(..., help="Path to the project directory"),
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Add a new project."""
|
|
57
|
+
config_manager = ConfigManager()
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# Resolve to absolute path
|
|
61
|
+
resolved_path = os.path.abspath(os.path.expanduser(path))
|
|
62
|
+
config_manager.add_project(name, resolved_path)
|
|
63
|
+
console.print(f"[green]Project '{name}' added at {format_path(resolved_path)}[/green]")
|
|
64
|
+
|
|
65
|
+
# Display usage hint
|
|
66
|
+
console.print("\nTo use this project:")
|
|
67
|
+
console.print(f" basic-memory --project={name} <command>")
|
|
68
|
+
console.print(" # or")
|
|
69
|
+
console.print(f" basic-memory project default {name}")
|
|
70
|
+
except ValueError as e:
|
|
71
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
72
|
+
raise typer.Exit(1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@project_app.command("remove")
|
|
76
|
+
def remove_project(
|
|
77
|
+
name: str = typer.Argument(..., help="Name of the project to remove"),
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Remove a project from configuration."""
|
|
80
|
+
config_manager = ConfigManager()
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
config_manager.remove_project(name)
|
|
84
|
+
console.print(f"[green]Project '{name}' removed from configuration[/green]")
|
|
85
|
+
console.print("[yellow]Note: The project files have not been deleted from disk.[/yellow]")
|
|
86
|
+
except ValueError as e: # pragma: no cover
|
|
87
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
88
|
+
raise typer.Exit(1)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@project_app.command("default")
|
|
92
|
+
def set_default_project(
|
|
93
|
+
name: str = typer.Argument(..., help="Name of the project to set as default"),
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Set the default project."""
|
|
96
|
+
config_manager = ConfigManager()
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
config_manager.set_default_project(name)
|
|
100
|
+
console.print(f"[green]Project '{name}' set as default[/green]")
|
|
101
|
+
except ValueError as e: # pragma: no cover
|
|
102
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
103
|
+
raise typer.Exit(1)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@project_app.command("current")
|
|
107
|
+
def show_current_project() -> None:
|
|
108
|
+
"""Show the current project."""
|
|
109
|
+
config_manager = ConfigManager()
|
|
110
|
+
current = os.environ.get("BASIC_MEMORY_PROJECT", config_manager.default_project)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
path = config_manager.get_project_path(current)
|
|
114
|
+
console.print(f"Current project: [cyan]{current}[/cyan]")
|
|
115
|
+
console.print(f"Path: [green]{format_path(str(path))}[/green]")
|
|
116
|
+
console.print(f"Database: [blue]{format_path(str(config.database_path))}[/blue]")
|
|
117
|
+
except ValueError: # pragma: no cover
|
|
118
|
+
console.print(f"[yellow]Warning: Project '{current}' not found in configuration[/yellow]")
|
|
119
|
+
console.print(f"Using default project: [cyan]{config_manager.default_project}[/cyan]")
|
|
@@ -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)
|