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.
- basic_memory/__init__.py +7 -0
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +185 -0
- basic_memory/alembic/migrations.py +24 -0
- basic_memory/alembic/script.py.mako +26 -0
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -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/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/api/__init__.py +5 -0
- basic_memory/api/app.py +131 -0
- basic_memory/api/routers/__init__.py +11 -0
- 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 +318 -0
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +90 -0
- basic_memory/api/routers/project_router.py +448 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +249 -0
- basic_memory/api/routers/search_router.py +36 -0
- 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 +182 -0
- basic_memory/api/v2/routers/knowledge_router.py +413 -0
- basic_memory/api/v2/routers/memory_router.py +130 -0
- basic_memory/api/v2/routers/project_router.py +342 -0
- basic_memory/api/v2/routers/prompt_router.py +270 -0
- basic_memory/api/v2/routers/resource_router.py +286 -0
- basic_memory/api/v2/routers/search_router.py +73 -0
- basic_memory/cli/__init__.py +1 -0
- basic_memory/cli/app.py +84 -0
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/__init__.py +18 -0
- basic_memory/cli/commands/cloud/__init__.py +6 -0
- basic_memory/cli/commands/cloud/api_client.py +112 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
- basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
- basic_memory/cli/commands/cloud/core_commands.py +195 -0
- basic_memory/cli/commands/cloud/rclone_commands.py +371 -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 +233 -0
- basic_memory/cli/commands/cloud/upload_command.py +124 -0
- basic_memory/cli/commands/command_utils.py +77 -0
- basic_memory/cli/commands/db.py +44 -0
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +84 -0
- basic_memory/cli/commands/import_claude_conversations.py +87 -0
- basic_memory/cli/commands/import_claude_projects.py +86 -0
- basic_memory/cli/commands/import_memory_json.py +87 -0
- basic_memory/cli/commands/mcp.py +76 -0
- basic_memory/cli/commands/project.py +889 -0
- basic_memory/cli/commands/status.py +174 -0
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/main.py +28 -0
- basic_memory/config.py +616 -0
- basic_memory/db.py +394 -0
- basic_memory/deps.py +705 -0
- basic_memory/file_utils.py +478 -0
- basic_memory/ignore_utils.py +297 -0
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +232 -0
- basic_memory/importers/claude_conversations_importer.py +180 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +108 -0
- basic_memory/importers/utils.py +61 -0
- basic_memory/markdown/__init__.py +21 -0
- basic_memory/markdown/entity_parser.py +279 -0
- basic_memory/markdown/markdown_processor.py +160 -0
- basic_memory/markdown/plugins.py +242 -0
- basic_memory/markdown/schemas.py +70 -0
- basic_memory/markdown/utils.py +117 -0
- basic_memory/mcp/__init__.py +1 -0
- basic_memory/mcp/async_client.py +139 -0
- basic_memory/mcp/project_context.py +141 -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 +81 -0
- basic_memory/mcp/tools/__init__.py +48 -0
- 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 +242 -0
- basic_memory/mcp/tools/edit_note.py +324 -0
- basic_memory/mcp/tools/list_directory.py +168 -0
- basic_memory/mcp/tools/move_note.py +551 -0
- basic_memory/mcp/tools/project_management.py +201 -0
- basic_memory/mcp/tools/read_content.py +281 -0
- basic_memory/mcp/tools/read_note.py +267 -0
- basic_memory/mcp/tools/recent_activity.py +534 -0
- basic_memory/mcp/tools/search.py +385 -0
- basic_memory/mcp/tools/utils.py +540 -0
- basic_memory/mcp/tools/view_note.py +78 -0
- basic_memory/mcp/tools/write_note.py +230 -0
- basic_memory/models/__init__.py +15 -0
- basic_memory/models/base.py +10 -0
- basic_memory/models/knowledge.py +226 -0
- basic_memory/models/project.py +87 -0
- basic_memory/models/search.py +85 -0
- basic_memory/repository/__init__.py +11 -0
- basic_memory/repository/entity_repository.py +503 -0
- basic_memory/repository/observation_repository.py +73 -0
- basic_memory/repository/postgres_search_repository.py +379 -0
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +128 -0
- basic_memory/repository/relation_repository.py +146 -0
- basic_memory/repository/repository.py +385 -0
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +94 -0
- basic_memory/repository/search_repository_base.py +241 -0
- basic_memory/repository/sqlite_search_repository.py +439 -0
- basic_memory/schemas/__init__.py +86 -0
- basic_memory/schemas/base.py +297 -0
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/delete.py +37 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +285 -0
- basic_memory/schemas/project_info.py +212 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +112 -0
- basic_memory/schemas/response.py +229 -0
- basic_memory/schemas/search.py +117 -0
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/schemas/v2/__init__.py +27 -0
- basic_memory/schemas/v2/entity.py +129 -0
- basic_memory/schemas/v2/resource.py +46 -0
- basic_memory/services/__init__.py +8 -0
- basic_memory/services/context_service.py +601 -0
- basic_memory/services/directory_service.py +308 -0
- basic_memory/services/entity_service.py +864 -0
- basic_memory/services/exceptions.py +37 -0
- basic_memory/services/file_service.py +541 -0
- basic_memory/services/initialization.py +216 -0
- basic_memory/services/link_resolver.py +121 -0
- basic_memory/services/project_service.py +880 -0
- basic_memory/services/search_service.py +404 -0
- basic_memory/services/service.py +15 -0
- basic_memory/sync/__init__.py +6 -0
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/sync_service.py +1259 -0
- basic_memory/sync/watch_service.py +510 -0
- 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 +468 -0
- basic_memory-0.17.1.dist-info/METADATA +617 -0
- basic_memory-0.17.1.dist-info/RECORD +171 -0
- basic_memory-0.17.1.dist-info/WHEEL +4 -0
- basic_memory-0.17.1.dist-info/entry_points.txt +3 -0
- 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)
|