basic-memory 0.17.1__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.
Files changed (171) hide show
  1. basic_memory/__init__.py +7 -0
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +185 -0
  4. basic_memory/alembic/migrations.py +24 -0
  5. basic_memory/alembic/script.py.mako +26 -0
  6. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  7. basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -0
  8. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  9. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
  10. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
  11. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  12. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  13. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  14. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  15. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
  16. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  17. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  18. basic_memory/api/__init__.py +5 -0
  19. basic_memory/api/app.py +131 -0
  20. basic_memory/api/routers/__init__.py +11 -0
  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 +318 -0
  24. basic_memory/api/routers/management_router.py +80 -0
  25. basic_memory/api/routers/memory_router.py +90 -0
  26. basic_memory/api/routers/project_router.py +448 -0
  27. basic_memory/api/routers/prompt_router.py +260 -0
  28. basic_memory/api/routers/resource_router.py +249 -0
  29. basic_memory/api/routers/search_router.py +36 -0
  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 +182 -0
  36. basic_memory/api/v2/routers/knowledge_router.py +413 -0
  37. basic_memory/api/v2/routers/memory_router.py +130 -0
  38. basic_memory/api/v2/routers/project_router.py +342 -0
  39. basic_memory/api/v2/routers/prompt_router.py +270 -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/__init__.py +1 -0
  43. basic_memory/cli/app.py +84 -0
  44. basic_memory/cli/auth.py +277 -0
  45. basic_memory/cli/commands/__init__.py +18 -0
  46. basic_memory/cli/commands/cloud/__init__.py +6 -0
  47. basic_memory/cli/commands/cloud/api_client.py +112 -0
  48. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  49. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  50. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  51. basic_memory/cli/commands/cloud/rclone_commands.py +371 -0
  52. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  53. basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
  54. basic_memory/cli/commands/cloud/upload.py +233 -0
  55. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  56. basic_memory/cli/commands/command_utils.py +77 -0
  57. basic_memory/cli/commands/db.py +44 -0
  58. basic_memory/cli/commands/format.py +198 -0
  59. basic_memory/cli/commands/import_chatgpt.py +84 -0
  60. basic_memory/cli/commands/import_claude_conversations.py +87 -0
  61. basic_memory/cli/commands/import_claude_projects.py +86 -0
  62. basic_memory/cli/commands/import_memory_json.py +87 -0
  63. basic_memory/cli/commands/mcp.py +76 -0
  64. basic_memory/cli/commands/project.py +889 -0
  65. basic_memory/cli/commands/status.py +174 -0
  66. basic_memory/cli/commands/telemetry.py +81 -0
  67. basic_memory/cli/commands/tool.py +341 -0
  68. basic_memory/cli/main.py +28 -0
  69. basic_memory/config.py +616 -0
  70. basic_memory/db.py +394 -0
  71. basic_memory/deps.py +705 -0
  72. basic_memory/file_utils.py +478 -0
  73. basic_memory/ignore_utils.py +297 -0
  74. basic_memory/importers/__init__.py +27 -0
  75. basic_memory/importers/base.py +79 -0
  76. basic_memory/importers/chatgpt_importer.py +232 -0
  77. basic_memory/importers/claude_conversations_importer.py +180 -0
  78. basic_memory/importers/claude_projects_importer.py +148 -0
  79. basic_memory/importers/memory_json_importer.py +108 -0
  80. basic_memory/importers/utils.py +61 -0
  81. basic_memory/markdown/__init__.py +21 -0
  82. basic_memory/markdown/entity_parser.py +279 -0
  83. basic_memory/markdown/markdown_processor.py +160 -0
  84. basic_memory/markdown/plugins.py +242 -0
  85. basic_memory/markdown/schemas.py +70 -0
  86. basic_memory/markdown/utils.py +117 -0
  87. basic_memory/mcp/__init__.py +1 -0
  88. basic_memory/mcp/async_client.py +139 -0
  89. basic_memory/mcp/project_context.py +141 -0
  90. basic_memory/mcp/prompts/__init__.py +19 -0
  91. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  92. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  93. basic_memory/mcp/prompts/recent_activity.py +188 -0
  94. basic_memory/mcp/prompts/search.py +57 -0
  95. basic_memory/mcp/prompts/utils.py +162 -0
  96. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  97. basic_memory/mcp/resources/project_info.py +71 -0
  98. basic_memory/mcp/server.py +81 -0
  99. basic_memory/mcp/tools/__init__.py +48 -0
  100. basic_memory/mcp/tools/build_context.py +120 -0
  101. basic_memory/mcp/tools/canvas.py +152 -0
  102. basic_memory/mcp/tools/chatgpt_tools.py +190 -0
  103. basic_memory/mcp/tools/delete_note.py +242 -0
  104. basic_memory/mcp/tools/edit_note.py +324 -0
  105. basic_memory/mcp/tools/list_directory.py +168 -0
  106. basic_memory/mcp/tools/move_note.py +551 -0
  107. basic_memory/mcp/tools/project_management.py +201 -0
  108. basic_memory/mcp/tools/read_content.py +281 -0
  109. basic_memory/mcp/tools/read_note.py +267 -0
  110. basic_memory/mcp/tools/recent_activity.py +534 -0
  111. basic_memory/mcp/tools/search.py +385 -0
  112. basic_memory/mcp/tools/utils.py +540 -0
  113. basic_memory/mcp/tools/view_note.py +78 -0
  114. basic_memory/mcp/tools/write_note.py +230 -0
  115. basic_memory/models/__init__.py +15 -0
  116. basic_memory/models/base.py +10 -0
  117. basic_memory/models/knowledge.py +226 -0
  118. basic_memory/models/project.py +87 -0
  119. basic_memory/models/search.py +85 -0
  120. basic_memory/repository/__init__.py +11 -0
  121. basic_memory/repository/entity_repository.py +503 -0
  122. basic_memory/repository/observation_repository.py +73 -0
  123. basic_memory/repository/postgres_search_repository.py +379 -0
  124. basic_memory/repository/project_info_repository.py +10 -0
  125. basic_memory/repository/project_repository.py +128 -0
  126. basic_memory/repository/relation_repository.py +146 -0
  127. basic_memory/repository/repository.py +385 -0
  128. basic_memory/repository/search_index_row.py +95 -0
  129. basic_memory/repository/search_repository.py +94 -0
  130. basic_memory/repository/search_repository_base.py +241 -0
  131. basic_memory/repository/sqlite_search_repository.py +439 -0
  132. basic_memory/schemas/__init__.py +86 -0
  133. basic_memory/schemas/base.py +297 -0
  134. basic_memory/schemas/cloud.py +50 -0
  135. basic_memory/schemas/delete.py +37 -0
  136. basic_memory/schemas/directory.py +30 -0
  137. basic_memory/schemas/importer.py +35 -0
  138. basic_memory/schemas/memory.py +285 -0
  139. basic_memory/schemas/project_info.py +212 -0
  140. basic_memory/schemas/prompt.py +90 -0
  141. basic_memory/schemas/request.py +112 -0
  142. basic_memory/schemas/response.py +229 -0
  143. basic_memory/schemas/search.py +117 -0
  144. basic_memory/schemas/sync_report.py +72 -0
  145. basic_memory/schemas/v2/__init__.py +27 -0
  146. basic_memory/schemas/v2/entity.py +129 -0
  147. basic_memory/schemas/v2/resource.py +46 -0
  148. basic_memory/services/__init__.py +8 -0
  149. basic_memory/services/context_service.py +601 -0
  150. basic_memory/services/directory_service.py +308 -0
  151. basic_memory/services/entity_service.py +864 -0
  152. basic_memory/services/exceptions.py +37 -0
  153. basic_memory/services/file_service.py +541 -0
  154. basic_memory/services/initialization.py +216 -0
  155. basic_memory/services/link_resolver.py +121 -0
  156. basic_memory/services/project_service.py +880 -0
  157. basic_memory/services/search_service.py +404 -0
  158. basic_memory/services/service.py +15 -0
  159. basic_memory/sync/__init__.py +6 -0
  160. basic_memory/sync/background_sync.py +26 -0
  161. basic_memory/sync/sync_service.py +1259 -0
  162. basic_memory/sync/watch_service.py +510 -0
  163. basic_memory/telemetry.py +249 -0
  164. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  165. basic_memory/templates/prompts/search.hbs +101 -0
  166. basic_memory/utils.py +468 -0
  167. basic_memory-0.17.1.dist-info/METADATA +617 -0
  168. basic_memory-0.17.1.dist-info/RECORD +171 -0
  169. basic_memory-0.17.1.dist-info/WHEEL +4 -0
  170. basic_memory-0.17.1.dist-info/entry_points.txt +3 -0
  171. basic_memory-0.17.1.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,124 @@
1
+ """Upload CLI commands for basic-memory projects."""
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ from rich.console import Console
8
+
9
+ from basic_memory.cli.app import cloud_app
10
+ from basic_memory.cli.commands.cloud.cloud_utils import (
11
+ create_cloud_project,
12
+ project_exists,
13
+ sync_project,
14
+ )
15
+ from basic_memory.cli.commands.cloud.upload import upload_path
16
+
17
+ console = Console()
18
+
19
+
20
+ @cloud_app.command("upload")
21
+ def upload(
22
+ path: Path = typer.Argument(
23
+ ...,
24
+ help="Path to local file or directory to upload",
25
+ exists=True,
26
+ readable=True,
27
+ resolve_path=True,
28
+ ),
29
+ project: str = typer.Option(
30
+ ...,
31
+ "--project",
32
+ "-p",
33
+ help="Cloud project name (destination)",
34
+ ),
35
+ create_project: bool = typer.Option(
36
+ False,
37
+ "--create-project",
38
+ "-c",
39
+ help="Create project if it doesn't exist",
40
+ ),
41
+ sync: bool = typer.Option(
42
+ True,
43
+ "--sync/--no-sync",
44
+ help="Sync project after upload (default: true)",
45
+ ),
46
+ verbose: bool = typer.Option(
47
+ False,
48
+ "--verbose",
49
+ "-v",
50
+ help="Show detailed information about file filtering and upload",
51
+ ),
52
+ no_gitignore: bool = typer.Option(
53
+ False,
54
+ "--no-gitignore",
55
+ help="Skip .gitignore patterns (still respects .bmignore)",
56
+ ),
57
+ dry_run: bool = typer.Option(
58
+ False,
59
+ "--dry-run",
60
+ help="Show what would be uploaded without actually uploading",
61
+ ),
62
+ ) -> None:
63
+ """Upload local files or directories to cloud project via WebDAV.
64
+
65
+ Examples:
66
+ bm cloud upload ~/my-notes --project research
67
+ bm cloud upload notes.md --project research --create-project
68
+ bm cloud upload ~/docs --project work --no-sync
69
+ bm cloud upload ./history --project proto --verbose
70
+ bm cloud upload ./notes --project work --no-gitignore
71
+ bm cloud upload ./files --project test --dry-run
72
+ """
73
+
74
+ async def _upload():
75
+ # Check if project exists
76
+ if not await project_exists(project):
77
+ if create_project:
78
+ console.print(f"[blue]Creating cloud project '{project}'...[/blue]")
79
+ try:
80
+ await create_cloud_project(project)
81
+ console.print(f"[green]Created project '{project}'[/green]")
82
+ except Exception as e:
83
+ console.print(f"[red]Failed to create project: {e}[/red]")
84
+ raise typer.Exit(1)
85
+ else:
86
+ console.print(
87
+ f"[red]Project '{project}' does not exist.[/red]\n"
88
+ f"[yellow]Options:[/yellow]\n"
89
+ f" 1. Create it first: bm project add {project}\n"
90
+ f" 2. Use --create-project flag to create automatically"
91
+ )
92
+ raise typer.Exit(1)
93
+
94
+ # Perform upload (or dry run)
95
+ if dry_run:
96
+ console.print(
97
+ f"[yellow]DRY RUN: Showing what would be uploaded to '{project}'[/yellow]"
98
+ )
99
+ else:
100
+ console.print(f"[blue]Uploading {path} to project '{project}'...[/blue]")
101
+
102
+ success = await upload_path(
103
+ path, project, verbose=verbose, use_gitignore=not no_gitignore, dry_run=dry_run
104
+ )
105
+ if not success:
106
+ console.print("[red]Upload failed[/red]")
107
+ raise typer.Exit(1)
108
+
109
+ if dry_run:
110
+ console.print("[yellow]DRY RUN complete - no files were uploaded[/yellow]")
111
+ else:
112
+ console.print(f"[green]Successfully uploaded to '{project}'[/green]")
113
+
114
+ # Sync project if requested (skip on dry run)
115
+ # Force full scan after bisync to ensure database is up-to-date with synced files
116
+ if sync and not dry_run:
117
+ console.print(f"[blue]Syncing project '{project}'...[/blue]")
118
+ try:
119
+ await sync_project(project, force_full=True)
120
+ except Exception as e:
121
+ console.print(f"[yellow]Warning: Sync failed: {e}[/yellow]")
122
+ console.print("[dim]Files uploaded but may not be indexed yet[/dim]")
123
+
124
+ asyncio.run(_upload())
@@ -0,0 +1,77 @@
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(project: Optional[str] = None, force_full: bool = False):
46
+ """Run sync operation via API endpoint.
47
+
48
+ Args:
49
+ project: Optional project name
50
+ force_full: If True, force a full scan bypassing watermark optimization
51
+ """
52
+
53
+ try:
54
+ async with get_client() as client:
55
+ project_item = await get_active_project(client, project, None)
56
+ url = f"{project_item.project_url}/project/sync"
57
+ if force_full:
58
+ url += "?force_full=true"
59
+ response = await call_post(client, url)
60
+ data = response.json()
61
+ console.print(f"[green]{data['message']}[/green]")
62
+ except (ToolError, ValueError) as e:
63
+ console.print(f"[red]Sync failed: {e}[/red]")
64
+ raise typer.Exit(1)
65
+
66
+
67
+ async def get_project_info(project: str):
68
+ """Get project information via API endpoint."""
69
+
70
+ try:
71
+ async with get_client() as client:
72
+ project_item = await get_active_project(client, project, None)
73
+ response = await call_get(client, f"{project_item.project_url}/project/info")
74
+ return ProjectInfoResponse.model_validate(response.json())
75
+ except (ToolError, ValueError) as e:
76
+ console.print(f"[red]Sync failed: {e}[/red]")
77
+ raise typer.Exit(1)
@@ -0,0 +1,44 @@
1
+ """Database management commands."""
2
+
3
+ import asyncio
4
+
5
+ import typer
6
+ from loguru import logger
7
+
8
+ from basic_memory import db
9
+ from basic_memory.cli.app import app
10
+ from basic_memory.config import ConfigManager, BasicMemoryConfig, save_basic_memory_config
11
+
12
+
13
+ @app.command()
14
+ def reset(
15
+ reindex: bool = typer.Option(False, "--reindex", help="Rebuild db index from filesystem"),
16
+ ): # pragma: no cover
17
+ """Reset database (drop all tables and recreate)."""
18
+ if typer.confirm("This will delete all data in your db. Are you sure?"):
19
+ logger.info("Resetting database...")
20
+ config_manager = ConfigManager()
21
+ app_config = config_manager.config
22
+ # Get database path
23
+ db_path = app_config.app_database_path
24
+
25
+ # Delete the database file if it exists
26
+ if db_path.exists():
27
+ db_path.unlink()
28
+ logger.info(f"Database file deleted: {db_path}")
29
+
30
+ # Reset project configuration
31
+ config = BasicMemoryConfig()
32
+ save_basic_memory_config(config_manager.config_file, config)
33
+ logger.info("Project configuration reset to default")
34
+
35
+ # Create a new empty database
36
+ asyncio.run(db.run_migrations(app_config))
37
+ logger.info("Database reset complete")
38
+
39
+ if reindex:
40
+ # Run database sync directly
41
+ from basic_memory.cli.commands.command_utils import run_sync
42
+
43
+ logger.info("Rebuilding search index from filesystem...")
44
+ asyncio.run(run_sync(project=None))
@@ -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
@@ -0,0 +1,84 @@
1
+ """Import command for ChatGPT conversations."""
2
+
3
+ import asyncio
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from basic_memory.cli.app import import_app
10
+ from basic_memory.config import ConfigManager, get_project_config
11
+ from basic_memory.importers import ChatGPTImporter
12
+ from basic_memory.markdown import EntityParser, MarkdownProcessor
13
+ from loguru import logger
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+
17
+ console = Console()
18
+
19
+
20
+ async def get_markdown_processor() -> MarkdownProcessor:
21
+ """Get MarkdownProcessor instance."""
22
+ config = get_project_config()
23
+ app_config = ConfigManager().config
24
+ entity_parser = EntityParser(config.home)
25
+ return MarkdownProcessor(entity_parser, app_config=app_config)
26
+
27
+
28
+ @import_app.command(name="chatgpt", help="Import conversations from ChatGPT JSON export.")
29
+ def import_chatgpt(
30
+ conversations_json: Annotated[
31
+ Path, typer.Argument(help="Path to ChatGPT conversations.json file")
32
+ ] = Path("conversations.json"),
33
+ folder: Annotated[
34
+ str, typer.Option(help="The folder to place the files in.")
35
+ ] = "conversations",
36
+ ):
37
+ """Import chat conversations from ChatGPT JSON format.
38
+
39
+ This command will:
40
+ 1. Read the complex tree structure of messages
41
+ 2. Convert them to linear markdown conversations
42
+ 3. Save as clean, readable markdown files
43
+
44
+ After importing, run 'basic-memory sync' to index the new files.
45
+ """
46
+
47
+ try:
48
+ if not conversations_json.exists(): # pragma: no cover
49
+ typer.echo(f"Error: File not found: {conversations_json}", err=True)
50
+ raise typer.Exit(1)
51
+
52
+ # Get markdown processor
53
+ markdown_processor = asyncio.run(get_markdown_processor())
54
+ config = get_project_config()
55
+ # Process the file
56
+ base_path = config.home / folder
57
+ console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
58
+
59
+ # Create importer and run import
60
+ importer = ChatGPTImporter(config.home, markdown_processor)
61
+ with conversations_json.open("r", encoding="utf-8") as file:
62
+ json_data = json.load(file)
63
+ result = asyncio.run(importer.import_data(json_data, folder))
64
+
65
+ if not result.success: # pragma: no cover
66
+ typer.echo(f"Error during import: {result.error_message}", err=True)
67
+ raise typer.Exit(1)
68
+
69
+ # Show results
70
+ console.print(
71
+ Panel(
72
+ f"[green]Import complete![/green]\n\n"
73
+ f"Imported {result.conversations} conversations\n"
74
+ f"Containing {result.messages} messages",
75
+ expand=False,
76
+ )
77
+ )
78
+
79
+ console.print("\nRun 'basic-memory sync' to index the new files.")
80
+
81
+ except Exception as e:
82
+ logger.error("Import failed")
83
+ typer.echo(f"Error during import: {e}", err=True)
84
+ raise typer.Exit(1)
@@ -0,0 +1,87 @@
1
+ """Import command for basic-memory CLI to import chat data from conversations2.json format."""
2
+
3
+ import asyncio
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from basic_memory.cli.app import claude_app
10
+ from basic_memory.config import ConfigManager, get_project_config
11
+ from basic_memory.importers.claude_conversations_importer import ClaudeConversationsImporter
12
+ from basic_memory.markdown import EntityParser, MarkdownProcessor
13
+ from loguru import logger
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+
17
+ console = Console()
18
+
19
+
20
+ async def get_markdown_processor() -> MarkdownProcessor:
21
+ """Get MarkdownProcessor instance."""
22
+ config = get_project_config()
23
+ app_config = ConfigManager().config
24
+ entity_parser = EntityParser(config.home)
25
+ return MarkdownProcessor(entity_parser, app_config=app_config)
26
+
27
+
28
+ @claude_app.command(name="conversations", help="Import chat conversations from Claude.ai.")
29
+ def import_claude(
30
+ conversations_json: Annotated[
31
+ Path, typer.Argument(..., help="Path to conversations.json file")
32
+ ] = Path("conversations.json"),
33
+ folder: Annotated[
34
+ str, typer.Option(help="The folder to place the files in.")
35
+ ] = "conversations",
36
+ ):
37
+ """Import chat conversations from conversations2.json format.
38
+
39
+ This command will:
40
+ 1. Read chat data and nested messages
41
+ 2. Create markdown files for each conversation
42
+ 3. Format content in clean, readable markdown
43
+
44
+ After importing, run 'basic-memory sync' to index the new files.
45
+ """
46
+
47
+ config = get_project_config()
48
+ try:
49
+ if not conversations_json.exists():
50
+ typer.echo(f"Error: File not found: {conversations_json}", err=True)
51
+ raise typer.Exit(1)
52
+
53
+ # Get markdown processor
54
+ markdown_processor = asyncio.run(get_markdown_processor())
55
+
56
+ # Create the importer
57
+ importer = ClaudeConversationsImporter(config.home, markdown_processor)
58
+
59
+ # Process the file
60
+ base_path = config.home / folder
61
+ console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
62
+
63
+ # Run the import
64
+ with conversations_json.open("r", encoding="utf-8") as file:
65
+ json_data = json.load(file)
66
+ result = asyncio.run(importer.import_data(json_data, folder))
67
+
68
+ if not result.success: # pragma: no cover
69
+ typer.echo(f"Error during import: {result.error_message}", err=True)
70
+ raise typer.Exit(1)
71
+
72
+ # Show results
73
+ console.print(
74
+ Panel(
75
+ f"[green]Import complete![/green]\n\n"
76
+ f"Imported {result.conversations} conversations\n"
77
+ f"Containing {result.messages} messages",
78
+ expand=False,
79
+ )
80
+ )
81
+
82
+ console.print("\nRun 'basic-memory sync' to index the new files.")
83
+
84
+ except Exception as e:
85
+ logger.error("Import failed")
86
+ typer.echo(f"Error during import: {e}", err=True)
87
+ raise typer.Exit(1)