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.

Files changed (195) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +130 -20
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  6. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  7. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
  8. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
  9. basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
  10. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  11. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  12. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  13. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  14. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
  15. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  16. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  17. basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
  18. basic_memory/api/app.py +87 -20
  19. basic_memory/api/container.py +133 -0
  20. basic_memory/api/routers/__init__.py +4 -1
  21. basic_memory/api/routers/directory_router.py +84 -0
  22. basic_memory/api/routers/importer_router.py +152 -0
  23. basic_memory/api/routers/knowledge_router.py +180 -23
  24. basic_memory/api/routers/management_router.py +80 -0
  25. basic_memory/api/routers/memory_router.py +9 -64
  26. basic_memory/api/routers/project_router.py +460 -0
  27. basic_memory/api/routers/prompt_router.py +260 -0
  28. basic_memory/api/routers/resource_router.py +136 -11
  29. basic_memory/api/routers/search_router.py +5 -5
  30. basic_memory/api/routers/utils.py +169 -0
  31. basic_memory/api/template_loader.py +292 -0
  32. basic_memory/api/v2/__init__.py +35 -0
  33. basic_memory/api/v2/routers/__init__.py +21 -0
  34. basic_memory/api/v2/routers/directory_router.py +93 -0
  35. basic_memory/api/v2/routers/importer_router.py +181 -0
  36. basic_memory/api/v2/routers/knowledge_router.py +427 -0
  37. basic_memory/api/v2/routers/memory_router.py +130 -0
  38. basic_memory/api/v2/routers/project_router.py +359 -0
  39. basic_memory/api/v2/routers/prompt_router.py +269 -0
  40. basic_memory/api/v2/routers/resource_router.py +286 -0
  41. basic_memory/api/v2/routers/search_router.py +73 -0
  42. basic_memory/cli/app.py +80 -10
  43. basic_memory/cli/auth.py +300 -0
  44. basic_memory/cli/commands/__init__.py +15 -2
  45. basic_memory/cli/commands/cloud/__init__.py +6 -0
  46. basic_memory/cli/commands/cloud/api_client.py +127 -0
  47. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  48. basic_memory/cli/commands/cloud/cloud_utils.py +108 -0
  49. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  50. basic_memory/cli/commands/cloud/rclone_commands.py +397 -0
  51. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  52. basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
  53. basic_memory/cli/commands/cloud/upload.py +240 -0
  54. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  55. basic_memory/cli/commands/command_utils.py +99 -0
  56. basic_memory/cli/commands/db.py +87 -12
  57. basic_memory/cli/commands/format.py +198 -0
  58. basic_memory/cli/commands/import_chatgpt.py +47 -223
  59. basic_memory/cli/commands/import_claude_conversations.py +48 -171
  60. basic_memory/cli/commands/import_claude_projects.py +53 -160
  61. basic_memory/cli/commands/import_memory_json.py +55 -111
  62. basic_memory/cli/commands/mcp.py +67 -11
  63. basic_memory/cli/commands/project.py +889 -0
  64. basic_memory/cli/commands/status.py +52 -34
  65. basic_memory/cli/commands/telemetry.py +81 -0
  66. basic_memory/cli/commands/tool.py +341 -0
  67. basic_memory/cli/container.py +84 -0
  68. basic_memory/cli/main.py +14 -6
  69. basic_memory/config.py +580 -26
  70. basic_memory/db.py +285 -28
  71. basic_memory/deps/__init__.py +293 -0
  72. basic_memory/deps/config.py +26 -0
  73. basic_memory/deps/db.py +56 -0
  74. basic_memory/deps/importers.py +200 -0
  75. basic_memory/deps/projects.py +238 -0
  76. basic_memory/deps/repositories.py +179 -0
  77. basic_memory/deps/services.py +480 -0
  78. basic_memory/deps.py +16 -185
  79. basic_memory/file_utils.py +318 -54
  80. basic_memory/ignore_utils.py +297 -0
  81. basic_memory/importers/__init__.py +27 -0
  82. basic_memory/importers/base.py +100 -0
  83. basic_memory/importers/chatgpt_importer.py +245 -0
  84. basic_memory/importers/claude_conversations_importer.py +192 -0
  85. basic_memory/importers/claude_projects_importer.py +184 -0
  86. basic_memory/importers/memory_json_importer.py +128 -0
  87. basic_memory/importers/utils.py +61 -0
  88. basic_memory/markdown/entity_parser.py +182 -23
  89. basic_memory/markdown/markdown_processor.py +70 -7
  90. basic_memory/markdown/plugins.py +43 -23
  91. basic_memory/markdown/schemas.py +1 -1
  92. basic_memory/markdown/utils.py +38 -14
  93. basic_memory/mcp/async_client.py +135 -4
  94. basic_memory/mcp/clients/__init__.py +28 -0
  95. basic_memory/mcp/clients/directory.py +70 -0
  96. basic_memory/mcp/clients/knowledge.py +176 -0
  97. basic_memory/mcp/clients/memory.py +120 -0
  98. basic_memory/mcp/clients/project.py +89 -0
  99. basic_memory/mcp/clients/resource.py +71 -0
  100. basic_memory/mcp/clients/search.py +65 -0
  101. basic_memory/mcp/container.py +110 -0
  102. basic_memory/mcp/project_context.py +155 -0
  103. basic_memory/mcp/prompts/__init__.py +19 -0
  104. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  105. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  106. basic_memory/mcp/prompts/recent_activity.py +188 -0
  107. basic_memory/mcp/prompts/search.py +57 -0
  108. basic_memory/mcp/prompts/utils.py +162 -0
  109. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  110. basic_memory/mcp/resources/project_info.py +71 -0
  111. basic_memory/mcp/server.py +61 -9
  112. basic_memory/mcp/tools/__init__.py +33 -21
  113. basic_memory/mcp/tools/build_context.py +120 -0
  114. basic_memory/mcp/tools/canvas.py +152 -0
  115. basic_memory/mcp/tools/chatgpt_tools.py +190 -0
  116. basic_memory/mcp/tools/delete_note.py +249 -0
  117. basic_memory/mcp/tools/edit_note.py +325 -0
  118. basic_memory/mcp/tools/list_directory.py +157 -0
  119. basic_memory/mcp/tools/move_note.py +549 -0
  120. basic_memory/mcp/tools/project_management.py +204 -0
  121. basic_memory/mcp/tools/read_content.py +281 -0
  122. basic_memory/mcp/tools/read_note.py +265 -0
  123. basic_memory/mcp/tools/recent_activity.py +528 -0
  124. basic_memory/mcp/tools/search.py +377 -24
  125. basic_memory/mcp/tools/utils.py +402 -16
  126. basic_memory/mcp/tools/view_note.py +78 -0
  127. basic_memory/mcp/tools/write_note.py +230 -0
  128. basic_memory/models/__init__.py +3 -2
  129. basic_memory/models/knowledge.py +82 -17
  130. basic_memory/models/project.py +93 -0
  131. basic_memory/models/search.py +68 -8
  132. basic_memory/project_resolver.py +222 -0
  133. basic_memory/repository/__init__.py +2 -0
  134. basic_memory/repository/entity_repository.py +437 -8
  135. basic_memory/repository/observation_repository.py +36 -3
  136. basic_memory/repository/postgres_search_repository.py +451 -0
  137. basic_memory/repository/project_info_repository.py +10 -0
  138. basic_memory/repository/project_repository.py +140 -0
  139. basic_memory/repository/relation_repository.py +79 -4
  140. basic_memory/repository/repository.py +148 -29
  141. basic_memory/repository/search_index_row.py +95 -0
  142. basic_memory/repository/search_repository.py +79 -268
  143. basic_memory/repository/search_repository_base.py +241 -0
  144. basic_memory/repository/sqlite_search_repository.py +437 -0
  145. basic_memory/runtime.py +61 -0
  146. basic_memory/schemas/__init__.py +22 -9
  147. basic_memory/schemas/base.py +131 -12
  148. basic_memory/schemas/cloud.py +50 -0
  149. basic_memory/schemas/directory.py +31 -0
  150. basic_memory/schemas/importer.py +35 -0
  151. basic_memory/schemas/memory.py +194 -25
  152. basic_memory/schemas/project_info.py +213 -0
  153. basic_memory/schemas/prompt.py +90 -0
  154. basic_memory/schemas/request.py +56 -2
  155. basic_memory/schemas/response.py +85 -28
  156. basic_memory/schemas/search.py +36 -35
  157. basic_memory/schemas/sync_report.py +72 -0
  158. basic_memory/schemas/v2/__init__.py +27 -0
  159. basic_memory/schemas/v2/entity.py +133 -0
  160. basic_memory/schemas/v2/resource.py +47 -0
  161. basic_memory/services/__init__.py +2 -1
  162. basic_memory/services/context_service.py +451 -138
  163. basic_memory/services/directory_service.py +310 -0
  164. basic_memory/services/entity_service.py +636 -71
  165. basic_memory/services/exceptions.py +21 -0
  166. basic_memory/services/file_service.py +402 -33
  167. basic_memory/services/initialization.py +216 -0
  168. basic_memory/services/link_resolver.py +50 -56
  169. basic_memory/services/project_service.py +888 -0
  170. basic_memory/services/search_service.py +232 -37
  171. basic_memory/sync/__init__.py +4 -2
  172. basic_memory/sync/background_sync.py +26 -0
  173. basic_memory/sync/coordinator.py +160 -0
  174. basic_memory/sync/sync_service.py +1200 -109
  175. basic_memory/sync/watch_service.py +432 -135
  176. basic_memory/telemetry.py +249 -0
  177. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  178. basic_memory/templates/prompts/search.hbs +101 -0
  179. basic_memory/utils.py +407 -54
  180. basic_memory-0.17.4.dist-info/METADATA +617 -0
  181. basic_memory-0.17.4.dist-info/RECORD +193 -0
  182. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
  183. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +1 -0
  184. basic_memory/alembic/README +0 -1
  185. basic_memory/cli/commands/sync.py +0 -206
  186. basic_memory/cli/commands/tools.py +0 -157
  187. basic_memory/mcp/tools/knowledge.py +0 -68
  188. basic_memory/mcp/tools/memory.py +0 -170
  189. basic_memory/mcp/tools/notes.py +0 -202
  190. basic_memory/schemas/discovery.py +0 -28
  191. basic_memory/sync/file_change_scanner.py +0 -158
  192. basic_memory/sync/utils.py +0 -31
  193. basic_memory-0.7.0.dist-info/METADATA +0 -378
  194. basic_memory-0.7.0.dist-info/RECORD +0 -82
  195. {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)
@@ -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.alembic import migrations
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 indices from filesystem"),
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
- with logfire.span("reset"): # pyright: ignore [reportGeneralTypeIssues]
19
- if typer.confirm("This will delete all data in your db. Are you sure?"):
20
- logger.info("Resetting database...")
21
- asyncio.run(migrations.reset_database())
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
- if reindex:
24
- # Import and run sync
25
- from basic_memory.cli.commands.sync import sync
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
- logger.info("Rebuilding search index from filesystem...")
28
- sync(watch=False) # pyright: ignore
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