basic-memory 0.7.0__py3-none-any.whl → 0.16.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.

Potentially problematic release.


This version of basic-memory might be problematic. Click here for more details.

Files changed (150) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +27 -3
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  6. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  7. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -0
  8. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  9. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  10. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  11. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +100 -0
  12. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  13. basic_memory/api/app.py +64 -18
  14. basic_memory/api/routers/__init__.py +4 -1
  15. basic_memory/api/routers/directory_router.py +84 -0
  16. basic_memory/api/routers/importer_router.py +152 -0
  17. basic_memory/api/routers/knowledge_router.py +166 -21
  18. basic_memory/api/routers/management_router.py +80 -0
  19. basic_memory/api/routers/memory_router.py +9 -64
  20. basic_memory/api/routers/project_router.py +406 -0
  21. basic_memory/api/routers/prompt_router.py +260 -0
  22. basic_memory/api/routers/resource_router.py +119 -4
  23. basic_memory/api/routers/search_router.py +5 -5
  24. basic_memory/api/routers/utils.py +130 -0
  25. basic_memory/api/template_loader.py +292 -0
  26. basic_memory/cli/app.py +43 -9
  27. basic_memory/cli/auth.py +277 -0
  28. basic_memory/cli/commands/__init__.py +13 -2
  29. basic_memory/cli/commands/cloud/__init__.py +6 -0
  30. basic_memory/cli/commands/cloud/api_client.py +112 -0
  31. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  32. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  33. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  34. basic_memory/cli/commands/cloud/rclone_commands.py +301 -0
  35. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  36. basic_memory/cli/commands/cloud/rclone_installer.py +249 -0
  37. basic_memory/cli/commands/cloud/upload.py +233 -0
  38. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  39. basic_memory/cli/commands/command_utils.py +51 -0
  40. basic_memory/cli/commands/db.py +28 -12
  41. basic_memory/cli/commands/import_chatgpt.py +40 -220
  42. basic_memory/cli/commands/import_claude_conversations.py +41 -168
  43. basic_memory/cli/commands/import_claude_projects.py +46 -157
  44. basic_memory/cli/commands/import_memory_json.py +48 -108
  45. basic_memory/cli/commands/mcp.py +84 -10
  46. basic_memory/cli/commands/project.py +876 -0
  47. basic_memory/cli/commands/status.py +50 -33
  48. basic_memory/cli/commands/tool.py +341 -0
  49. basic_memory/cli/main.py +8 -7
  50. basic_memory/config.py +477 -23
  51. basic_memory/db.py +168 -17
  52. basic_memory/deps.py +251 -25
  53. basic_memory/file_utils.py +113 -58
  54. basic_memory/ignore_utils.py +297 -0
  55. basic_memory/importers/__init__.py +27 -0
  56. basic_memory/importers/base.py +79 -0
  57. basic_memory/importers/chatgpt_importer.py +232 -0
  58. basic_memory/importers/claude_conversations_importer.py +177 -0
  59. basic_memory/importers/claude_projects_importer.py +148 -0
  60. basic_memory/importers/memory_json_importer.py +108 -0
  61. basic_memory/importers/utils.py +58 -0
  62. basic_memory/markdown/entity_parser.py +143 -23
  63. basic_memory/markdown/markdown_processor.py +3 -3
  64. basic_memory/markdown/plugins.py +39 -21
  65. basic_memory/markdown/schemas.py +1 -1
  66. basic_memory/markdown/utils.py +28 -13
  67. basic_memory/mcp/async_client.py +134 -4
  68. basic_memory/mcp/project_context.py +141 -0
  69. basic_memory/mcp/prompts/__init__.py +19 -0
  70. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  71. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  72. basic_memory/mcp/prompts/recent_activity.py +188 -0
  73. basic_memory/mcp/prompts/search.py +57 -0
  74. basic_memory/mcp/prompts/utils.py +162 -0
  75. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  76. basic_memory/mcp/resources/project_info.py +71 -0
  77. basic_memory/mcp/server.py +7 -13
  78. basic_memory/mcp/tools/__init__.py +33 -21
  79. basic_memory/mcp/tools/build_context.py +120 -0
  80. basic_memory/mcp/tools/canvas.py +130 -0
  81. basic_memory/mcp/tools/chatgpt_tools.py +187 -0
  82. basic_memory/mcp/tools/delete_note.py +225 -0
  83. basic_memory/mcp/tools/edit_note.py +320 -0
  84. basic_memory/mcp/tools/list_directory.py +167 -0
  85. basic_memory/mcp/tools/move_note.py +545 -0
  86. basic_memory/mcp/tools/project_management.py +200 -0
  87. basic_memory/mcp/tools/read_content.py +271 -0
  88. basic_memory/mcp/tools/read_note.py +255 -0
  89. basic_memory/mcp/tools/recent_activity.py +534 -0
  90. basic_memory/mcp/tools/search.py +369 -23
  91. basic_memory/mcp/tools/utils.py +374 -16
  92. basic_memory/mcp/tools/view_note.py +77 -0
  93. basic_memory/mcp/tools/write_note.py +207 -0
  94. basic_memory/models/__init__.py +3 -2
  95. basic_memory/models/knowledge.py +67 -15
  96. basic_memory/models/project.py +87 -0
  97. basic_memory/models/search.py +10 -6
  98. basic_memory/repository/__init__.py +2 -0
  99. basic_memory/repository/entity_repository.py +229 -7
  100. basic_memory/repository/observation_repository.py +35 -3
  101. basic_memory/repository/project_info_repository.py +10 -0
  102. basic_memory/repository/project_repository.py +103 -0
  103. basic_memory/repository/relation_repository.py +21 -2
  104. basic_memory/repository/repository.py +147 -29
  105. basic_memory/repository/search_repository.py +411 -62
  106. basic_memory/schemas/__init__.py +22 -9
  107. basic_memory/schemas/base.py +97 -8
  108. basic_memory/schemas/cloud.py +50 -0
  109. basic_memory/schemas/directory.py +30 -0
  110. basic_memory/schemas/importer.py +35 -0
  111. basic_memory/schemas/memory.py +187 -25
  112. basic_memory/schemas/project_info.py +211 -0
  113. basic_memory/schemas/prompt.py +90 -0
  114. basic_memory/schemas/request.py +56 -2
  115. basic_memory/schemas/response.py +1 -1
  116. basic_memory/schemas/search.py +31 -35
  117. basic_memory/schemas/sync_report.py +72 -0
  118. basic_memory/services/__init__.py +2 -1
  119. basic_memory/services/context_service.py +241 -104
  120. basic_memory/services/directory_service.py +295 -0
  121. basic_memory/services/entity_service.py +590 -60
  122. basic_memory/services/exceptions.py +21 -0
  123. basic_memory/services/file_service.py +284 -30
  124. basic_memory/services/initialization.py +191 -0
  125. basic_memory/services/link_resolver.py +49 -56
  126. basic_memory/services/project_service.py +863 -0
  127. basic_memory/services/search_service.py +168 -32
  128. basic_memory/sync/__init__.py +3 -2
  129. basic_memory/sync/background_sync.py +26 -0
  130. basic_memory/sync/sync_service.py +1180 -109
  131. basic_memory/sync/watch_service.py +412 -135
  132. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  133. basic_memory/templates/prompts/search.hbs +101 -0
  134. basic_memory/utils.py +383 -51
  135. basic_memory-0.16.1.dist-info/METADATA +493 -0
  136. basic_memory-0.16.1.dist-info/RECORD +148 -0
  137. {basic_memory-0.7.0.dist-info → basic_memory-0.16.1.dist-info}/entry_points.txt +1 -0
  138. basic_memory/alembic/README +0 -1
  139. basic_memory/cli/commands/sync.py +0 -206
  140. basic_memory/cli/commands/tools.py +0 -157
  141. basic_memory/mcp/tools/knowledge.py +0 -68
  142. basic_memory/mcp/tools/memory.py +0 -170
  143. basic_memory/mcp/tools/notes.py +0 -202
  144. basic_memory/schemas/discovery.py +0 -28
  145. basic_memory/sync/file_change_scanner.py +0 -158
  146. basic_memory/sync/utils.py +0 -31
  147. basic_memory-0.7.0.dist-info/METADATA +0 -378
  148. basic_memory-0.7.0.dist-info/RECORD +0 -82
  149. {basic_memory-0.7.0.dist-info → basic_memory-0.16.1.dist-info}/WHEEL +0 -0
  150. {basic_memory-0.7.0.dist-info → basic_memory-0.16.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,100 @@
1
+ """Update search index schema
2
+
3
+ Revision ID: cc7172b46608
4
+ Revises: 502b60eaa905
5
+ Create Date: 2025-02-28 18:48:23.244941
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ from alembic import op
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = "cc7172b46608"
16
+ down_revision: Union[str, None] = "502b60eaa905"
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade database schema to use new search index with content_stems and content_snippet."""
23
+
24
+ # First, drop the existing search_index table
25
+ op.execute("DROP TABLE IF EXISTS search_index")
26
+
27
+ # Create new search_index with updated schema
28
+ op.execute("""
29
+ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
30
+ -- Core entity fields
31
+ id UNINDEXED, -- Row ID
32
+ title, -- Title for searching
33
+ content_stems, -- Main searchable content split into stems
34
+ content_snippet, -- File content snippet for display
35
+ permalink, -- Stable identifier (now indexed for path search)
36
+ file_path UNINDEXED, -- Physical location
37
+ type UNINDEXED, -- entity/relation/observation
38
+
39
+ -- Relation fields
40
+ from_id UNINDEXED, -- Source entity
41
+ to_id UNINDEXED, -- Target entity
42
+ relation_type UNINDEXED, -- Type of relation
43
+
44
+ -- Observation fields
45
+ entity_id UNINDEXED, -- Parent entity
46
+ category UNINDEXED, -- Observation category
47
+
48
+ -- Common fields
49
+ metadata UNINDEXED, -- JSON metadata
50
+ created_at UNINDEXED, -- Creation timestamp
51
+ updated_at UNINDEXED, -- Last update
52
+
53
+ -- Configuration
54
+ tokenize='unicode61 tokenchars 0x2F', -- Hex code for /
55
+ prefix='1,2,3,4' -- Support longer prefixes for paths
56
+ );
57
+ """)
58
+
59
+
60
+ def downgrade() -> None:
61
+ """Downgrade database schema to use old search index."""
62
+ # Drop the updated search_index table
63
+ op.execute("DROP TABLE IF EXISTS search_index")
64
+
65
+ # Recreate the original search_index schema
66
+ op.execute("""
67
+ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
68
+ -- Core entity fields
69
+ id UNINDEXED, -- Row ID
70
+ title, -- Title for searching
71
+ content, -- Main searchable content
72
+ permalink, -- Stable identifier (now indexed for path search)
73
+ file_path UNINDEXED, -- Physical location
74
+ type UNINDEXED, -- entity/relation/observation
75
+
76
+ -- Relation fields
77
+ from_id UNINDEXED, -- Source entity
78
+ to_id UNINDEXED, -- Target entity
79
+ relation_type UNINDEXED, -- Type of relation
80
+
81
+ -- Observation fields
82
+ entity_id UNINDEXED, -- Parent entity
83
+ category UNINDEXED, -- Observation category
84
+
85
+ -- Common fields
86
+ metadata UNINDEXED, -- JSON metadata
87
+ created_at UNINDEXED, -- Creation timestamp
88
+ updated_at UNINDEXED, -- Last update
89
+
90
+ -- Configuration
91
+ tokenize='unicode61 tokenchars 0x2F', -- Hex code for /
92
+ prefix='1,2,3,4' -- Support longer prefixes for paths
93
+ );
94
+ """)
95
+
96
+ # Print instruction to manually reindex after migration
97
+ print("\n------------------------------------------------------------------")
98
+ print("IMPORTANT: After downgrade completes, manually run the reindex command:")
99
+ print("basic-memory sync")
100
+ print("------------------------------------------------------------------\n")
@@ -0,0 +1,37 @@
1
+ """Add scan watermark tracking to Project
2
+
3
+ Revision ID: e7e1f4367280
4
+ Revises: 9d9c1cb7d8f5
5
+ Create Date: 2025-10-20 16:42:46.625075
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ from alembic import op
12
+ import sqlalchemy as sa
13
+
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = "e7e1f4367280"
17
+ down_revision: Union[str, None] = "9d9c1cb7d8f5"
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ with op.batch_alter_table("project", schema=None) as batch_op:
25
+ batch_op.add_column(sa.Column("last_scan_timestamp", sa.Float(), nullable=True))
26
+ batch_op.add_column(sa.Column("last_file_count", sa.Integer(), nullable=True))
27
+
28
+ # ### end Alembic commands ###
29
+
30
+
31
+ def downgrade() -> None:
32
+ # ### commands auto generated by Alembic - please adjust! ###
33
+ with op.batch_alter_table("project", schema=None) as batch_op:
34
+ batch_op.drop_column("last_file_count")
35
+ batch_op.drop_column("last_scan_timestamp")
36
+
37
+ # ### end Alembic commands ###
basic_memory/api/app.py CHANGED
@@ -1,27 +1,60 @@
1
1
  """FastAPI application for basic-memory knowledge graph API."""
2
2
 
3
+ import asyncio
3
4
  from contextlib import asynccontextmanager
4
5
 
5
- import logfire
6
6
  from fastapi import FastAPI, HTTPException
7
7
  from fastapi.exception_handlers import http_exception_handler
8
8
  from loguru import logger
9
9
 
10
- import basic_memory
10
+ from basic_memory import __version__ as version
11
11
  from basic_memory import db
12
- from basic_memory.config import config as app_config
13
- from basic_memory.api.routers import knowledge, search, memory, resource
14
- from basic_memory.utils import setup_logging
12
+ from basic_memory.api.routers import (
13
+ directory_router,
14
+ importer_router,
15
+ knowledge,
16
+ management,
17
+ memory,
18
+ project,
19
+ resource,
20
+ search,
21
+ prompt_router,
22
+ )
23
+ from basic_memory.config import ConfigManager
24
+ from basic_memory.services.initialization import initialize_file_sync, initialize_app
15
25
 
16
26
 
17
27
  @asynccontextmanager
18
28
  async def lifespan(app: FastAPI): # pragma: no cover
19
- """Lifecycle manager for the FastAPI app."""
20
- setup_logging(log_file=".basic-memory/basic-memory.log")
21
- logger.info(f"Starting Basic Memory API {basic_memory.__version__}")
22
- await db.run_migrations(app_config)
29
+ """Lifecycle manager for the FastAPI app. Not called in stdio mcp mode"""
30
+
31
+ app_config = ConfigManager().config
32
+ logger.info("Starting Basic Memory API")
33
+
34
+ await initialize_app(app_config)
35
+
36
+ # Cache database connections in app state for performance
37
+ logger.info("Initializing database and caching connections...")
38
+ engine, session_maker = await db.get_or_create_db(app_config.database_path)
39
+ app.state.engine = engine
40
+ app.state.session_maker = session_maker
41
+ logger.info("Database connections cached in app state")
42
+
43
+ logger.info(f"Sync changes enabled: {app_config.sync_changes}")
44
+ if app_config.sync_changes:
45
+ # start file sync task in background
46
+ app.state.sync_task = asyncio.create_task(initialize_file_sync(app_config))
47
+ else:
48
+ logger.info("Sync changes disabled. Skipping file sync service.")
49
+
50
+ # proceed with startup
23
51
  yield
52
+
24
53
  logger.info("Shutting down Basic Memory API")
54
+ if app.state.sync_task:
55
+ logger.info("Stopping sync...")
56
+ app.state.sync_task.cancel() # pyright: ignore
57
+
25
58
  await db.shutdown_db()
26
59
 
27
60
 
@@ -29,24 +62,37 @@ async def lifespan(app: FastAPI): # pragma: no cover
29
62
  app = FastAPI(
30
63
  title="Basic Memory API",
31
64
  description="Knowledge graph API for basic-memory",
32
- version="0.1.0",
65
+ version=version,
33
66
  lifespan=lifespan,
34
67
  )
35
68
 
36
- if app_config != "test":
37
- logfire.instrument_fastapi(app)
38
-
39
69
 
40
70
  # Include routers
41
- app.include_router(knowledge.router)
42
- app.include_router(search.router)
43
- app.include_router(memory.router)
44
- app.include_router(resource.router)
71
+ app.include_router(knowledge.router, prefix="/{project}")
72
+ app.include_router(memory.router, prefix="/{project}")
73
+ app.include_router(resource.router, prefix="/{project}")
74
+ app.include_router(search.router, prefix="/{project}")
75
+ app.include_router(project.project_router, prefix="/{project}")
76
+ app.include_router(directory_router.router, prefix="/{project}")
77
+ app.include_router(prompt_router.router, prefix="/{project}")
78
+ app.include_router(importer_router.router, prefix="/{project}")
79
+
80
+ # Project resource router works accross projects
81
+ app.include_router(project.project_resource_router)
82
+ app.include_router(management.router)
83
+
84
+ # Auth routes are handled by FastMCP automatically when auth is enabled
45
85
 
46
86
 
47
87
  @app.exception_handler(Exception)
48
88
  async def exception_handler(request, exc): # pragma: no cover
49
89
  logger.exception(
50
- f"An unhandled exception occurred for request '{request.url}', exception: {exc}"
90
+ "API unhandled exception",
91
+ url=str(request.url),
92
+ method=request.method,
93
+ client=request.client.host if request.client else None,
94
+ path=request.url.path,
95
+ error_type=type(exc).__name__,
96
+ error=str(exc),
51
97
  )
52
98
  return await http_exception_handler(request, HTTPException(status_code=500, detail=str(exc)))
@@ -1,8 +1,11 @@
1
1
  """API routers."""
2
2
 
3
3
  from . import knowledge_router as knowledge
4
+ from . import management_router as management
4
5
  from . import memory_router as memory
6
+ from . import project_router as project
5
7
  from . import resource_router as resource
6
8
  from . import search_router as search
9
+ from . import prompt_router as prompt
7
10
 
8
- __all__ = ["knowledge", "memory", "resource", "search"]
11
+ __all__ = ["knowledge", "management", "memory", "project", "resource", "search", "prompt"]
@@ -0,0 +1,84 @@
1
+ """Router for directory tree operations."""
2
+
3
+ from typing import List, Optional
4
+
5
+ from fastapi import APIRouter, Query
6
+
7
+ from basic_memory.deps import DirectoryServiceDep, ProjectIdDep
8
+ from basic_memory.schemas.directory import DirectoryNode
9
+
10
+ router = APIRouter(prefix="/directory", tags=["directory"])
11
+
12
+
13
+ @router.get("/tree", response_model=DirectoryNode, response_model_exclude_none=True)
14
+ async def get_directory_tree(
15
+ directory_service: DirectoryServiceDep,
16
+ project_id: ProjectIdDep,
17
+ ):
18
+ """Get hierarchical directory structure from the knowledge base.
19
+
20
+ Args:
21
+ directory_service: Service for directory operations
22
+ project_id: ID of the current project
23
+
24
+ Returns:
25
+ DirectoryNode representing the root of the hierarchical tree structure
26
+ """
27
+ # Get a hierarchical directory tree for the specific project
28
+ tree = await directory_service.get_directory_tree()
29
+
30
+ # Return the hierarchical tree
31
+ return tree
32
+
33
+
34
+ @router.get("/structure", response_model=DirectoryNode, response_model_exclude_none=True)
35
+ async def get_directory_structure(
36
+ directory_service: DirectoryServiceDep,
37
+ project_id: ProjectIdDep,
38
+ ):
39
+ """Get folder structure for navigation (no files).
40
+
41
+ Optimized endpoint for folder tree navigation. Returns only directory nodes
42
+ without file metadata. For full tree with files, use /directory/tree.
43
+
44
+ Args:
45
+ directory_service: Service for directory operations
46
+ project_id: ID of the current project
47
+
48
+ Returns:
49
+ DirectoryNode tree containing only folders (type="directory")
50
+ """
51
+ structure = await directory_service.get_directory_structure()
52
+ return structure
53
+
54
+
55
+ @router.get("/list", response_model=List[DirectoryNode], response_model_exclude_none=True)
56
+ async def list_directory(
57
+ directory_service: DirectoryServiceDep,
58
+ project_id: ProjectIdDep,
59
+ dir_name: str = Query("/", description="Directory path to list"),
60
+ depth: int = Query(1, ge=1, le=10, description="Recursion depth (1-10)"),
61
+ file_name_glob: Optional[str] = Query(
62
+ None, description="Glob pattern for filtering file names"
63
+ ),
64
+ ):
65
+ """List directory contents with filtering and depth control.
66
+
67
+ Args:
68
+ directory_service: Service for directory operations
69
+ project_id: ID of the current project
70
+ dir_name: Directory path to list (default: root "/")
71
+ depth: Recursion depth (1-10, default: 1 for immediate children only)
72
+ file_name_glob: Optional glob pattern for filtering file names (e.g., "*.md", "*meeting*")
73
+
74
+ Returns:
75
+ List of DirectoryNode objects matching the criteria
76
+ """
77
+ # Get directory listing with filtering
78
+ nodes = await directory_service.list_directory(
79
+ dir_name=dir_name,
80
+ depth=depth,
81
+ file_name_glob=file_name_glob,
82
+ )
83
+
84
+ return nodes
@@ -0,0 +1,152 @@
1
+ """Import router for Basic Memory API."""
2
+
3
+ import json
4
+ import logging
5
+
6
+ from fastapi import APIRouter, Form, HTTPException, UploadFile, status
7
+
8
+ from basic_memory.deps import (
9
+ ChatGPTImporterDep,
10
+ ClaudeConversationsImporterDep,
11
+ ClaudeProjectsImporterDep,
12
+ MemoryJsonImporterDep,
13
+ )
14
+ from basic_memory.importers import Importer
15
+ from basic_memory.schemas.importer import (
16
+ ChatImportResult,
17
+ EntityImportResult,
18
+ ProjectImportResult,
19
+ )
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ router = APIRouter(prefix="/import", tags=["import"])
24
+
25
+
26
+ @router.post("/chatgpt", response_model=ChatImportResult)
27
+ async def import_chatgpt(
28
+ importer: ChatGPTImporterDep,
29
+ file: UploadFile,
30
+ folder: str = Form("conversations"),
31
+ ) -> ChatImportResult:
32
+ """Import conversations from ChatGPT JSON export.
33
+
34
+ Args:
35
+ file: The ChatGPT conversations.json file.
36
+ folder: The folder to place the files in.
37
+ markdown_processor: MarkdownProcessor instance.
38
+
39
+ Returns:
40
+ ChatImportResult with import statistics.
41
+
42
+ Raises:
43
+ HTTPException: If import fails.
44
+ """
45
+ return await import_file(importer, file, folder)
46
+
47
+
48
+ @router.post("/claude/conversations", response_model=ChatImportResult)
49
+ async def import_claude_conversations(
50
+ importer: ClaudeConversationsImporterDep,
51
+ file: UploadFile,
52
+ folder: str = Form("conversations"),
53
+ ) -> ChatImportResult:
54
+ """Import conversations from Claude conversations.json export.
55
+
56
+ Args:
57
+ file: The Claude conversations.json file.
58
+ folder: The folder to place the files in.
59
+ markdown_processor: MarkdownProcessor instance.
60
+
61
+ Returns:
62
+ ChatImportResult with import statistics.
63
+
64
+ Raises:
65
+ HTTPException: If import fails.
66
+ """
67
+ return await import_file(importer, file, folder)
68
+
69
+
70
+ @router.post("/claude/projects", response_model=ProjectImportResult)
71
+ async def import_claude_projects(
72
+ importer: ClaudeProjectsImporterDep,
73
+ file: UploadFile,
74
+ folder: str = Form("projects"),
75
+ ) -> ProjectImportResult:
76
+ """Import projects from Claude projects.json export.
77
+
78
+ Args:
79
+ file: The Claude projects.json file.
80
+ base_folder: The base folder to place the files in.
81
+ markdown_processor: MarkdownProcessor instance.
82
+
83
+ Returns:
84
+ ProjectImportResult with import statistics.
85
+
86
+ Raises:
87
+ HTTPException: If import fails.
88
+ """
89
+ return await import_file(importer, file, folder)
90
+
91
+
92
+ @router.post("/memory-json", response_model=EntityImportResult)
93
+ async def import_memory_json(
94
+ importer: MemoryJsonImporterDep,
95
+ file: UploadFile,
96
+ folder: str = Form("conversations"),
97
+ ) -> EntityImportResult:
98
+ """Import entities and relations from a memory.json file.
99
+
100
+ Args:
101
+ file: The memory.json file.
102
+ destination_folder: Optional destination folder within the project.
103
+ markdown_processor: MarkdownProcessor instance.
104
+
105
+ Returns:
106
+ EntityImportResult with import statistics.
107
+
108
+ Raises:
109
+ HTTPException: If import fails.
110
+ """
111
+ try:
112
+ file_data = []
113
+ file_bytes = await file.read()
114
+ file_str = file_bytes.decode("utf-8")
115
+ for line in file_str.splitlines():
116
+ json_data = json.loads(line)
117
+ file_data.append(json_data)
118
+
119
+ result = await importer.import_data(file_data, folder)
120
+ if not result.success: # pragma: no cover
121
+ raise HTTPException(
122
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
123
+ detail=result.error_message or "Import failed",
124
+ )
125
+ except Exception as e:
126
+ logger.exception("Import failed")
127
+ raise HTTPException(
128
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
129
+ detail=f"Import failed: {str(e)}",
130
+ )
131
+ return result
132
+
133
+
134
+ async def import_file(importer: Importer, file: UploadFile, destination_folder: str):
135
+ try:
136
+ # Process file
137
+ json_data = json.load(file.file)
138
+ result = await importer.import_data(json_data, destination_folder)
139
+ if not result.success: # pragma: no cover
140
+ raise HTTPException(
141
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
142
+ detail=result.error_message or "Import failed",
143
+ )
144
+
145
+ return result
146
+
147
+ except Exception as e:
148
+ logger.exception("Import failed")
149
+ raise HTTPException(
150
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
151
+ detail=f"Import failed: {str(e)}",
152
+ )