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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""utility functions for commands"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Optional, TypeVar, Coroutine, Any
|
|
5
|
+
|
|
6
|
+
from mcp.server.fastmcp.exceptions import ToolError
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from basic_memory import db
|
|
12
|
+
from basic_memory.mcp.async_client import get_client
|
|
13
|
+
|
|
14
|
+
from basic_memory.mcp.tools.utils import call_post, call_get
|
|
15
|
+
from basic_memory.mcp.project_context import get_active_project
|
|
16
|
+
from basic_memory.schemas import ProjectInfoResponse
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run_with_cleanup(coro: Coroutine[Any, Any, T]) -> T:
|
|
24
|
+
"""Run an async coroutine with proper database cleanup.
|
|
25
|
+
|
|
26
|
+
This helper ensures database connections are cleaned up before the event
|
|
27
|
+
loop closes, preventing process hangs in CLI commands.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
coro: The coroutine to run
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The result of the coroutine
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
async def _with_cleanup() -> T:
|
|
37
|
+
try:
|
|
38
|
+
return await coro
|
|
39
|
+
finally:
|
|
40
|
+
await db.shutdown_db()
|
|
41
|
+
|
|
42
|
+
return asyncio.run(_with_cleanup())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def run_sync(
|
|
46
|
+
project: Optional[str] = None,
|
|
47
|
+
force_full: bool = False,
|
|
48
|
+
run_in_background: bool = True,
|
|
49
|
+
):
|
|
50
|
+
"""Run sync operation via API endpoint.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
project: Optional project name
|
|
54
|
+
force_full: If True, force a full scan bypassing watermark optimization
|
|
55
|
+
run_in_background: If True, return immediately; if False, wait for completion
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
async with get_client() as client:
|
|
60
|
+
project_item = await get_active_project(client, project, None)
|
|
61
|
+
url = f"{project_item.project_url}/project/sync"
|
|
62
|
+
params = []
|
|
63
|
+
if force_full:
|
|
64
|
+
params.append("force_full=true")
|
|
65
|
+
if not run_in_background:
|
|
66
|
+
params.append("run_in_background=false")
|
|
67
|
+
if params:
|
|
68
|
+
url += "?" + "&".join(params)
|
|
69
|
+
response = await call_post(client, url)
|
|
70
|
+
data = response.json()
|
|
71
|
+
# Background mode returns {"message": "..."}, foreground returns SyncReportResponse
|
|
72
|
+
if "message" in data:
|
|
73
|
+
console.print(f"[green]{data['message']}[/green]")
|
|
74
|
+
else:
|
|
75
|
+
# Foreground mode - show summary of sync results
|
|
76
|
+
total = data.get("total", 0)
|
|
77
|
+
new_count = len(data.get("new", []))
|
|
78
|
+
modified_count = len(data.get("modified", []))
|
|
79
|
+
deleted_count = len(data.get("deleted", []))
|
|
80
|
+
console.print(
|
|
81
|
+
f"[green]Synced {total} files[/green] "
|
|
82
|
+
f"(new: {new_count}, modified: {modified_count}, deleted: {deleted_count})"
|
|
83
|
+
)
|
|
84
|
+
except (ToolError, ValueError) as e:
|
|
85
|
+
console.print(f"[red]Sync failed: {e}[/red]")
|
|
86
|
+
raise typer.Exit(1)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def get_project_info(project: str):
|
|
90
|
+
"""Get project information via API endpoint."""
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
async with get_client() as client:
|
|
94
|
+
project_item = await get_active_project(client, project, None)
|
|
95
|
+
response = await call_get(client, f"{project_item.project_url}/project/info")
|
|
96
|
+
return ProjectInfoResponse.model_validate(response.json())
|
|
97
|
+
except (ToolError, ValueError) as e:
|
|
98
|
+
console.print(f"[red]Sync failed: {e}[/red]")
|
|
99
|
+
raise typer.Exit(1)
|
basic_memory/cli/commands/db.py
CHANGED
|
@@ -1,28 +1,103 @@
|
|
|
1
1
|
"""Database management commands."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
|
-
import logfire
|
|
6
6
|
import typer
|
|
7
7
|
from loguru import logger
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from sqlalchemy.exc import OperationalError
|
|
8
10
|
|
|
9
|
-
from basic_memory
|
|
11
|
+
from basic_memory import db
|
|
10
12
|
from basic_memory.cli.app import app
|
|
13
|
+
from basic_memory.config import ConfigManager
|
|
14
|
+
from basic_memory.repository import ProjectRepository
|
|
15
|
+
from basic_memory.services.initialization import reconcile_projects_with_config
|
|
16
|
+
from basic_memory.sync.sync_service import get_sync_service
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def _reindex_projects(app_config):
|
|
22
|
+
"""Reindex all projects in a single async context.
|
|
23
|
+
|
|
24
|
+
This ensures all database operations use the same event loop,
|
|
25
|
+
and proper cleanup happens when the function completes.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
await reconcile_projects_with_config(app_config)
|
|
29
|
+
|
|
30
|
+
# Get database session (migrations already run if needed)
|
|
31
|
+
_, session_maker = await db.get_or_create_db(
|
|
32
|
+
db_path=app_config.database_path,
|
|
33
|
+
db_type=db.DatabaseType.FILESYSTEM,
|
|
34
|
+
)
|
|
35
|
+
project_repository = ProjectRepository(session_maker)
|
|
36
|
+
projects = await project_repository.get_active_projects()
|
|
37
|
+
|
|
38
|
+
for project in projects:
|
|
39
|
+
console.print(f" Indexing [cyan]{project.name}[/cyan]...")
|
|
40
|
+
logger.info(f"Starting sync for project: {project.name}")
|
|
41
|
+
sync_service = await get_sync_service(project)
|
|
42
|
+
sync_dir = Path(project.path)
|
|
43
|
+
await sync_service.sync(sync_dir, project_name=project.name)
|
|
44
|
+
logger.info(f"Sync completed for project: {project.name}")
|
|
45
|
+
finally:
|
|
46
|
+
# Clean up database connections before event loop closes
|
|
47
|
+
await db.shutdown_db()
|
|
11
48
|
|
|
12
49
|
|
|
13
50
|
@app.command()
|
|
14
51
|
def reset(
|
|
15
|
-
reindex: bool = typer.Option(False, "--reindex", help="Rebuild
|
|
52
|
+
reindex: bool = typer.Option(False, "--reindex", help="Rebuild db index from filesystem"),
|
|
16
53
|
): # pragma: no cover
|
|
17
54
|
"""Reset database (drop all tables and recreate)."""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
55
|
+
console.print(
|
|
56
|
+
"[yellow]Note:[/yellow] This only deletes the index database. "
|
|
57
|
+
"Your markdown note files will not be affected.\n"
|
|
58
|
+
"Use [green]bm reset --reindex[/green] to automatically rebuild the index afterward."
|
|
59
|
+
)
|
|
60
|
+
if typer.confirm("Reset the database index?"):
|
|
61
|
+
logger.info("Resetting database...")
|
|
62
|
+
config_manager = ConfigManager()
|
|
63
|
+
app_config = config_manager.config
|
|
64
|
+
# Get database path
|
|
65
|
+
db_path = app_config.app_database_path
|
|
66
|
+
|
|
67
|
+
# Delete the database file and WAL files if they exist
|
|
68
|
+
for suffix in ["", "-shm", "-wal"]:
|
|
69
|
+
path = db_path.parent / f"{db_path.name}{suffix}"
|
|
70
|
+
if path.exists():
|
|
71
|
+
try:
|
|
72
|
+
path.unlink()
|
|
73
|
+
logger.info(f"Deleted: {path}")
|
|
74
|
+
except OSError as e:
|
|
75
|
+
console.print(
|
|
76
|
+
f"[red]Error:[/red] Cannot delete {path.name}: {e}\n"
|
|
77
|
+
"The database may be in use by another process (e.g., MCP server).\n"
|
|
78
|
+
"Please close Claude Desktop or any other Basic Memory clients and try again."
|
|
79
|
+
)
|
|
80
|
+
raise typer.Exit(1)
|
|
22
81
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
82
|
+
# Create a new empty database (preserves project configuration)
|
|
83
|
+
try:
|
|
84
|
+
asyncio.run(db.run_migrations(app_config))
|
|
85
|
+
except OperationalError as e:
|
|
86
|
+
if "disk I/O error" in str(e) or "database is locked" in str(e):
|
|
87
|
+
console.print(
|
|
88
|
+
"[red]Error:[/red] Cannot access database. "
|
|
89
|
+
"It may be in use by another process (e.g., MCP server).\n"
|
|
90
|
+
"Please close Claude Desktop or any other Basic Memory clients and try again."
|
|
91
|
+
)
|
|
92
|
+
raise typer.Exit(1)
|
|
93
|
+
raise
|
|
94
|
+
console.print("[green]Database reset complete[/green]")
|
|
26
95
|
|
|
27
|
-
|
|
28
|
-
|
|
96
|
+
if reindex:
|
|
97
|
+
projects = list(app_config.projects)
|
|
98
|
+
if not projects:
|
|
99
|
+
console.print("[yellow]No projects configured. Skipping reindex.[/yellow]")
|
|
100
|
+
else:
|
|
101
|
+
console.print(f"Rebuilding search index for {len(projects)} project(s)...")
|
|
102
|
+
asyncio.run(_reindex_projects(app_config))
|
|
103
|
+
console.print("[green]Reindex complete[/green]")
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Format command for basic-memory CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated, Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
11
|
+
|
|
12
|
+
from basic_memory.cli.app import app
|
|
13
|
+
from basic_memory.config import ConfigManager, get_project_config
|
|
14
|
+
from basic_memory.file_utils import format_file
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_markdown_extension(path: Path) -> bool:
|
|
20
|
+
"""Check if file has a markdown extension."""
|
|
21
|
+
return path.suffix.lower() in (".md", ".markdown")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def format_single_file(file_path: Path, app_config) -> tuple[Path, bool, Optional[str]]:
|
|
25
|
+
"""Format a single file.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Tuple of (path, success, error_message)
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
result = await format_file(
|
|
32
|
+
file_path, app_config, is_markdown=is_markdown_extension(file_path)
|
|
33
|
+
)
|
|
34
|
+
if result is not None:
|
|
35
|
+
return (file_path, True, None)
|
|
36
|
+
else:
|
|
37
|
+
return (file_path, False, "No formatter configured or formatting skipped")
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return (file_path, False, str(e))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def format_files(
|
|
43
|
+
paths: list[Path], app_config, show_progress: bool = True
|
|
44
|
+
) -> tuple[int, int, list[tuple[Path, str]]]:
|
|
45
|
+
"""Format multiple files.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Tuple of (formatted_count, skipped_count, errors)
|
|
49
|
+
"""
|
|
50
|
+
formatted = 0
|
|
51
|
+
skipped = 0
|
|
52
|
+
errors: list[tuple[Path, str]] = []
|
|
53
|
+
|
|
54
|
+
if show_progress:
|
|
55
|
+
with Progress(
|
|
56
|
+
SpinnerColumn(),
|
|
57
|
+
TextColumn("[progress.description]{task.description}"),
|
|
58
|
+
console=console,
|
|
59
|
+
) as progress:
|
|
60
|
+
task = progress.add_task("Formatting files...", total=len(paths))
|
|
61
|
+
|
|
62
|
+
for file_path in paths:
|
|
63
|
+
path, success, error = await format_single_file(file_path, app_config)
|
|
64
|
+
if success:
|
|
65
|
+
formatted += 1
|
|
66
|
+
elif error and "No formatter configured" not in error:
|
|
67
|
+
errors.append((path, error))
|
|
68
|
+
else:
|
|
69
|
+
skipped += 1
|
|
70
|
+
progress.update(task, advance=1)
|
|
71
|
+
else:
|
|
72
|
+
for file_path in paths:
|
|
73
|
+
path, success, error = await format_single_file(file_path, app_config)
|
|
74
|
+
if success:
|
|
75
|
+
formatted += 1
|
|
76
|
+
elif error and "No formatter configured" not in error:
|
|
77
|
+
errors.append((path, error))
|
|
78
|
+
else:
|
|
79
|
+
skipped += 1
|
|
80
|
+
|
|
81
|
+
return formatted, skipped, errors
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def run_format(
|
|
85
|
+
path: Optional[Path] = None,
|
|
86
|
+
project: Optional[str] = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Run the format command."""
|
|
89
|
+
app_config = ConfigManager().config
|
|
90
|
+
|
|
91
|
+
# Check if formatting is enabled
|
|
92
|
+
if (
|
|
93
|
+
not app_config.format_on_save
|
|
94
|
+
and not app_config.formatter_command
|
|
95
|
+
and not app_config.formatters
|
|
96
|
+
):
|
|
97
|
+
console.print(
|
|
98
|
+
"[yellow]No formatters configured. Set format_on_save=true and "
|
|
99
|
+
"formatter_command or formatters in your config.[/yellow]"
|
|
100
|
+
)
|
|
101
|
+
console.print(
|
|
102
|
+
"\nExample config (~/.basic-memory/config.json):\n"
|
|
103
|
+
' "format_on_save": true,\n'
|
|
104
|
+
' "formatter_command": "prettier --write {file}"\n'
|
|
105
|
+
)
|
|
106
|
+
raise typer.Exit(1)
|
|
107
|
+
|
|
108
|
+
# Temporarily enable format_on_save for this command
|
|
109
|
+
# (so format_file actually runs the formatter)
|
|
110
|
+
original_format_on_save = app_config.format_on_save
|
|
111
|
+
app_config.format_on_save = True
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# Determine which files to format
|
|
115
|
+
if path:
|
|
116
|
+
# Format specific file or directory
|
|
117
|
+
if path.is_file():
|
|
118
|
+
files = [path]
|
|
119
|
+
elif path.is_dir():
|
|
120
|
+
# Find all markdown and json files
|
|
121
|
+
files = (
|
|
122
|
+
list(path.rglob("*.md"))
|
|
123
|
+
+ list(path.rglob("*.json"))
|
|
124
|
+
+ list(path.rglob("*.canvas"))
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
console.print(f"[red]Path not found: {path}[/red]")
|
|
128
|
+
raise typer.Exit(1)
|
|
129
|
+
else:
|
|
130
|
+
# Format all files in project
|
|
131
|
+
project_config = get_project_config(project)
|
|
132
|
+
project_path = Path(project_config.home)
|
|
133
|
+
|
|
134
|
+
if not project_path.exists():
|
|
135
|
+
console.print(f"[red]Project path not found: {project_path}[/red]")
|
|
136
|
+
raise typer.Exit(1)
|
|
137
|
+
|
|
138
|
+
# Find all markdown and json files
|
|
139
|
+
files = (
|
|
140
|
+
list(project_path.rglob("*.md"))
|
|
141
|
+
+ list(project_path.rglob("*.json"))
|
|
142
|
+
+ list(project_path.rglob("*.canvas"))
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if not files:
|
|
146
|
+
console.print("[yellow]No files found to format.[/yellow]")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
console.print(f"Found {len(files)} file(s) to format...")
|
|
150
|
+
|
|
151
|
+
formatted, skipped, errors = await format_files(files, app_config)
|
|
152
|
+
|
|
153
|
+
# Print summary
|
|
154
|
+
console.print()
|
|
155
|
+
if formatted > 0:
|
|
156
|
+
console.print(f"[green]Formatted: {formatted} file(s)[/green]")
|
|
157
|
+
if skipped > 0:
|
|
158
|
+
console.print(f"[dim]Skipped: {skipped} file(s) (no formatter for extension)[/dim]")
|
|
159
|
+
if errors:
|
|
160
|
+
console.print(f"[red]Errors: {len(errors)} file(s)[/red]")
|
|
161
|
+
for path, error in errors:
|
|
162
|
+
console.print(f" [red]{path}[/red]: {error}")
|
|
163
|
+
|
|
164
|
+
finally:
|
|
165
|
+
# Restore original setting
|
|
166
|
+
app_config.format_on_save = original_format_on_save
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@app.command()
|
|
170
|
+
def format(
|
|
171
|
+
path: Annotated[
|
|
172
|
+
Optional[Path],
|
|
173
|
+
typer.Argument(help="File or directory to format. Defaults to current project."),
|
|
174
|
+
] = None,
|
|
175
|
+
project: Annotated[
|
|
176
|
+
Optional[str],
|
|
177
|
+
typer.Option("--project", "-p", help="Project name to format."),
|
|
178
|
+
] = None,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Format files using configured formatters.
|
|
181
|
+
|
|
182
|
+
Uses the formatter_command or formatters settings from your config.
|
|
183
|
+
By default, formats all .md, .json, and .canvas files in the current project.
|
|
184
|
+
|
|
185
|
+
Examples:
|
|
186
|
+
basic-memory format # Format all files in current project
|
|
187
|
+
basic-memory format --project research # Format files in specific project
|
|
188
|
+
basic-memory format notes/meeting.md # Format a specific file
|
|
189
|
+
basic-memory format notes/ # Format all files in directory
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
asyncio.run(run_format(path, project))
|
|
193
|
+
except Exception as e:
|
|
194
|
+
if not isinstance(e, typer.Exit):
|
|
195
|
+
logger.error(f"Error formatting files: {e}")
|
|
196
|
+
console.print(f"[red]Error formatting files: {e}[/red]")
|
|
197
|
+
raise typer.Exit(code=1)
|
|
198
|
+
raise
|