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,232 @@
1
+ """ChatGPT import service for Basic Memory."""
2
+
3
+ import logging
4
+ from datetime import datetime
5
+ from typing import Any, Dict, List, Optional, Set
6
+
7
+ from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown
8
+ from basic_memory.importers.base import Importer
9
+ from basic_memory.schemas.importer import ChatImportResult
10
+ from basic_memory.importers.utils import clean_filename, format_timestamp
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class ChatGPTImporter(Importer[ChatImportResult]):
16
+ """Service for importing ChatGPT conversations."""
17
+
18
+ async def import_data(
19
+ self, source_data, destination_folder: str, **kwargs: Any
20
+ ) -> ChatImportResult:
21
+ """Import conversations from ChatGPT JSON export.
22
+
23
+ Args:
24
+ source_path: Path to the ChatGPT conversations.json file.
25
+ destination_folder: Destination folder within the project.
26
+ **kwargs: Additional keyword arguments.
27
+
28
+ Returns:
29
+ ChatImportResult containing statistics and status of the import.
30
+ """
31
+ try: # pragma: no cover
32
+ # Ensure the destination folder exists
33
+ self.ensure_folder_exists(destination_folder)
34
+ conversations = source_data
35
+
36
+ # Process each conversation
37
+ messages_imported = 0
38
+ chats_imported = 0
39
+
40
+ for chat in conversations:
41
+ # Convert to entity
42
+ entity = self._format_chat_content(destination_folder, chat)
43
+
44
+ # Write file
45
+ file_path = self.base_path / f"{entity.frontmatter.metadata['permalink']}.md"
46
+ await self.write_entity(entity, file_path)
47
+
48
+ # Count messages
49
+ msg_count = sum(
50
+ 1
51
+ for node in chat["mapping"].values()
52
+ if node.get("message")
53
+ and not node.get("message", {})
54
+ .get("metadata", {})
55
+ .get("is_visually_hidden_from_conversation")
56
+ )
57
+
58
+ chats_imported += 1
59
+ messages_imported += msg_count
60
+
61
+ return ChatImportResult(
62
+ import_count={"conversations": chats_imported, "messages": messages_imported},
63
+ success=True,
64
+ conversations=chats_imported,
65
+ messages=messages_imported,
66
+ )
67
+
68
+ except Exception as e: # pragma: no cover
69
+ logger.exception("Failed to import ChatGPT conversations")
70
+ return self.handle_error("Failed to import ChatGPT conversations", e) # pyright: ignore [reportReturnType]
71
+
72
+ def _format_chat_content(
73
+ self, folder: str, conversation: Dict[str, Any]
74
+ ) -> EntityMarkdown: # pragma: no cover
75
+ """Convert chat conversation to Basic Memory entity.
76
+
77
+ Args:
78
+ folder: Destination folder name.
79
+ conversation: ChatGPT conversation data.
80
+
81
+ Returns:
82
+ EntityMarkdown instance representing the conversation.
83
+ """
84
+ # Extract timestamps
85
+ created_at = conversation["create_time"]
86
+ modified_at = conversation["update_time"]
87
+
88
+ root_id = None
89
+ # Find root message
90
+ for node_id, node in conversation["mapping"].items():
91
+ if node.get("parent") is None:
92
+ root_id = node_id
93
+ break
94
+
95
+ # Generate permalink
96
+ date_prefix = datetime.fromtimestamp(created_at).astimezone().strftime("%Y%m%d")
97
+ clean_title = clean_filename(conversation["title"])
98
+
99
+ # Format content
100
+ content = self._format_chat_markdown(
101
+ title=conversation["title"],
102
+ mapping=conversation["mapping"],
103
+ root_id=root_id,
104
+ created_at=created_at,
105
+ modified_at=modified_at,
106
+ )
107
+
108
+ # Create entity
109
+ entity = EntityMarkdown(
110
+ frontmatter=EntityFrontmatter(
111
+ metadata={
112
+ "type": "conversation",
113
+ "title": conversation["title"],
114
+ "created": format_timestamp(created_at),
115
+ "modified": format_timestamp(modified_at),
116
+ "permalink": f"{folder}/{date_prefix}-{clean_title}",
117
+ }
118
+ ),
119
+ content=content,
120
+ )
121
+
122
+ return entity
123
+
124
+ def _format_chat_markdown(
125
+ self,
126
+ title: str,
127
+ mapping: Dict[str, Any],
128
+ root_id: Optional[str],
129
+ created_at: float,
130
+ modified_at: float,
131
+ ) -> str: # pragma: no cover
132
+ """Format chat as clean markdown.
133
+
134
+ Args:
135
+ title: Chat title.
136
+ mapping: Message mapping.
137
+ root_id: Root message ID.
138
+ created_at: Creation timestamp.
139
+ modified_at: Modification timestamp.
140
+
141
+ Returns:
142
+ Formatted markdown content.
143
+ """
144
+ # Start with title
145
+ lines = [f"# {title}\n"]
146
+
147
+ # Traverse message tree
148
+ seen_msgs: Set[str] = set()
149
+ messages = self._traverse_messages(mapping, root_id, seen_msgs)
150
+
151
+ # Format each message
152
+ for msg in messages:
153
+ # Skip hidden messages
154
+ if msg.get("metadata", {}).get("is_visually_hidden_from_conversation"):
155
+ continue
156
+
157
+ # Get author and timestamp
158
+ author = msg["author"]["role"].title()
159
+ ts = format_timestamp(msg["create_time"]) if msg.get("create_time") else ""
160
+
161
+ # Add message header
162
+ lines.append(f"### {author} ({ts})")
163
+
164
+ # Add message content
165
+ content = self._get_message_content(msg)
166
+ if content:
167
+ lines.append(content)
168
+
169
+ # Add spacing
170
+ lines.append("")
171
+
172
+ return "\n".join(lines)
173
+
174
+ def _get_message_content(self, message: Dict[str, Any]) -> str: # pragma: no cover
175
+ """Extract clean message content.
176
+
177
+ Args:
178
+ message: Message data.
179
+
180
+ Returns:
181
+ Cleaned message content.
182
+ """
183
+ if not message or "content" not in message:
184
+ return ""
185
+
186
+ content = message["content"]
187
+ if content.get("content_type") == "text":
188
+ return "\n".join(content.get("parts", []))
189
+ elif content.get("content_type") == "code":
190
+ return f"```{content.get('language', '')}\n{content.get('text', '')}\n```"
191
+ return ""
192
+
193
+ def _traverse_messages(
194
+ self, mapping: Dict[str, Any], root_id: Optional[str], seen: Set[str]
195
+ ) -> List[Dict[str, Any]]: # pragma: no cover
196
+ """Traverse message tree iteratively to handle deep conversations.
197
+
198
+ Args:
199
+ mapping: Message mapping.
200
+ root_id: Root message ID.
201
+ seen: Set of seen message IDs.
202
+
203
+ Returns:
204
+ List of message data.
205
+ """
206
+ messages = []
207
+ if not root_id:
208
+ return messages
209
+
210
+ # Use iterative approach with stack to avoid recursion depth issues
211
+ stack = [root_id]
212
+
213
+ while stack:
214
+ node_id = stack.pop()
215
+ if not node_id:
216
+ continue
217
+
218
+ node = mapping.get(node_id)
219
+ if not node:
220
+ continue
221
+
222
+ # Process current node if it has a message and hasn't been seen
223
+ if node["id"] not in seen and node.get("message"):
224
+ seen.add(node["id"])
225
+ messages.append(node["message"])
226
+
227
+ # Add children to stack in reverse order to maintain conversation flow
228
+ children = node.get("children", [])
229
+ for child_id in reversed(children):
230
+ stack.append(child_id)
231
+
232
+ return messages
@@ -0,0 +1,177 @@
1
+ """Claude conversations import service for Basic Memory."""
2
+
3
+ import logging
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List
7
+
8
+ from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown
9
+ from basic_memory.importers.base import Importer
10
+ from basic_memory.schemas.importer import ChatImportResult
11
+ from basic_memory.importers.utils import clean_filename, format_timestamp
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ClaudeConversationsImporter(Importer[ChatImportResult]):
17
+ """Service for importing Claude conversations."""
18
+
19
+ async def import_data(
20
+ self, source_data, destination_folder: str, **kwargs: Any
21
+ ) -> ChatImportResult:
22
+ """Import conversations from Claude JSON export.
23
+
24
+ Args:
25
+ source_data: Path to the Claude conversations.json file.
26
+ destination_folder: Destination folder within the project.
27
+ **kwargs: Additional keyword arguments.
28
+
29
+ Returns:
30
+ ChatImportResult containing statistics and status of the import.
31
+ """
32
+ try:
33
+ # Ensure the destination folder exists
34
+ folder_path = self.ensure_folder_exists(destination_folder)
35
+
36
+ conversations = source_data
37
+
38
+ # Process each conversation
39
+ messages_imported = 0
40
+ chats_imported = 0
41
+
42
+ for chat in conversations:
43
+ # Convert to entity
44
+ entity = self._format_chat_content(
45
+ base_path=folder_path,
46
+ name=chat["name"],
47
+ messages=chat["chat_messages"],
48
+ created_at=chat["created_at"],
49
+ modified_at=chat["updated_at"],
50
+ )
51
+
52
+ # Write file
53
+ file_path = self.base_path / Path(f"{entity.frontmatter.metadata['permalink']}.md")
54
+ await self.write_entity(entity, file_path)
55
+
56
+ chats_imported += 1
57
+ messages_imported += len(chat["chat_messages"])
58
+
59
+ return ChatImportResult(
60
+ import_count={"conversations": chats_imported, "messages": messages_imported},
61
+ success=True,
62
+ conversations=chats_imported,
63
+ messages=messages_imported,
64
+ )
65
+
66
+ except Exception as e: # pragma: no cover
67
+ logger.exception("Failed to import Claude conversations")
68
+ return self.handle_error("Failed to import Claude conversations", e) # pyright: ignore [reportReturnType]
69
+
70
+ def _format_chat_content(
71
+ self,
72
+ base_path: Path,
73
+ name: str,
74
+ messages: List[Dict[str, Any]],
75
+ created_at: str,
76
+ modified_at: str,
77
+ ) -> EntityMarkdown:
78
+ """Convert chat messages to Basic Memory entity format.
79
+
80
+ Args:
81
+ base_path: Base path for the entity.
82
+ name: Chat name.
83
+ messages: List of chat messages.
84
+ created_at: Creation timestamp.
85
+ modified_at: Modification timestamp.
86
+
87
+ Returns:
88
+ EntityMarkdown instance representing the conversation.
89
+ """
90
+ # Generate permalink
91
+ date_prefix = datetime.fromisoformat(created_at.replace("Z", "+00:00")).strftime("%Y%m%d")
92
+ clean_title = clean_filename(name)
93
+ permalink = f"{base_path.name}/{date_prefix}-{clean_title}"
94
+
95
+ # Format content
96
+ content = self._format_chat_markdown(
97
+ name=name,
98
+ messages=messages,
99
+ created_at=created_at,
100
+ modified_at=modified_at,
101
+ permalink=permalink,
102
+ )
103
+
104
+ # Create entity
105
+ entity = EntityMarkdown(
106
+ frontmatter=EntityFrontmatter(
107
+ metadata={
108
+ "type": "conversation",
109
+ "title": name,
110
+ "created": created_at,
111
+ "modified": modified_at,
112
+ "permalink": permalink,
113
+ }
114
+ ),
115
+ content=content,
116
+ )
117
+
118
+ return entity
119
+
120
+ def _format_chat_markdown(
121
+ self,
122
+ name: str,
123
+ messages: List[Dict[str, Any]],
124
+ created_at: str,
125
+ modified_at: str,
126
+ permalink: str,
127
+ ) -> str:
128
+ """Format chat as clean markdown.
129
+
130
+ Args:
131
+ name: Chat name.
132
+ messages: List of chat messages.
133
+ created_at: Creation timestamp.
134
+ modified_at: Modification timestamp.
135
+ permalink: Permalink for the entity.
136
+
137
+ Returns:
138
+ Formatted markdown content.
139
+ """
140
+ # Start with frontmatter and title
141
+ lines = [
142
+ f"# {name}\n",
143
+ ]
144
+
145
+ # Add messages
146
+ for msg in messages:
147
+ # Format timestamp
148
+ ts = format_timestamp(msg["created_at"])
149
+
150
+ # Add message header
151
+ lines.append(f"### {msg['sender'].title()} ({ts})")
152
+
153
+ # Handle message content
154
+ content = msg.get("text", "")
155
+ if msg.get("content"):
156
+ # Filter out None values before joining
157
+ content = " ".join(
158
+ str(c.get("text", ""))
159
+ for c in msg["content"]
160
+ if c and c.get("text") is not None
161
+ )
162
+ lines.append(content)
163
+
164
+ # Handle attachments
165
+ attachments = msg.get("attachments", [])
166
+ for attachment in attachments:
167
+ if "file_name" in attachment:
168
+ lines.append(f"\n**Attachment: {attachment['file_name']}**")
169
+ if "extracted_content" in attachment:
170
+ lines.append("```")
171
+ lines.append(attachment["extracted_content"])
172
+ lines.append("```")
173
+
174
+ # Add spacing between messages
175
+ lines.append("")
176
+
177
+ return "\n".join(lines)
@@ -0,0 +1,148 @@
1
+ """Claude projects import service for Basic Memory."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, Optional
5
+
6
+ from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown
7
+ from basic_memory.importers.base import Importer
8
+ from basic_memory.schemas.importer import ProjectImportResult
9
+ from basic_memory.importers.utils import clean_filename
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class ClaudeProjectsImporter(Importer[ProjectImportResult]):
15
+ """Service for importing Claude projects."""
16
+
17
+ async def import_data(
18
+ self, source_data, destination_folder: str, **kwargs: Any
19
+ ) -> ProjectImportResult:
20
+ """Import projects from Claude JSON export.
21
+
22
+ Args:
23
+ source_path: Path to the Claude projects.json file.
24
+ destination_folder: Base folder for projects within the project.
25
+ **kwargs: Additional keyword arguments.
26
+
27
+ Returns:
28
+ ProjectImportResult containing statistics and status of the import.
29
+ """
30
+ try:
31
+ # Ensure the base folder exists
32
+ base_path = self.base_path
33
+ if destination_folder:
34
+ base_path = self.ensure_folder_exists(destination_folder)
35
+
36
+ projects = source_data
37
+
38
+ # Process each project
39
+ docs_imported = 0
40
+ prompts_imported = 0
41
+
42
+ for project in projects:
43
+ project_dir = clean_filename(project["name"])
44
+
45
+ # Create project directories
46
+ docs_dir = base_path / project_dir / "docs"
47
+ docs_dir.mkdir(parents=True, exist_ok=True)
48
+
49
+ # Import prompt template if it exists
50
+ if prompt_entity := self._format_prompt_markdown(project):
51
+ file_path = base_path / f"{prompt_entity.frontmatter.metadata['permalink']}.md"
52
+ await self.write_entity(prompt_entity, file_path)
53
+ prompts_imported += 1
54
+
55
+ # Import project documents
56
+ for doc in project.get("docs", []):
57
+ entity = self._format_project_markdown(project, doc)
58
+ file_path = base_path / f"{entity.frontmatter.metadata['permalink']}.md"
59
+ await self.write_entity(entity, file_path)
60
+ docs_imported += 1
61
+
62
+ return ProjectImportResult(
63
+ import_count={"documents": docs_imported, "prompts": prompts_imported},
64
+ success=True,
65
+ documents=docs_imported,
66
+ prompts=prompts_imported,
67
+ )
68
+
69
+ except Exception as e: # pragma: no cover
70
+ logger.exception("Failed to import Claude projects")
71
+ return self.handle_error("Failed to import Claude projects", e) # pyright: ignore [reportReturnType]
72
+
73
+ def _format_project_markdown(
74
+ self, project: Dict[str, Any], doc: Dict[str, Any]
75
+ ) -> EntityMarkdown:
76
+ """Format a project document as a Basic Memory entity.
77
+
78
+ Args:
79
+ project: Project data.
80
+ doc: Document data.
81
+
82
+ Returns:
83
+ EntityMarkdown instance representing the document.
84
+ """
85
+ # Extract timestamps
86
+ created_at = doc.get("created_at") or project["created_at"]
87
+ modified_at = project["updated_at"]
88
+
89
+ # Generate clean names for organization
90
+ project_dir = clean_filename(project["name"])
91
+ doc_file = clean_filename(doc["filename"])
92
+
93
+ # Create entity
94
+ entity = EntityMarkdown(
95
+ frontmatter=EntityFrontmatter(
96
+ metadata={
97
+ "type": "project_doc",
98
+ "title": doc["filename"],
99
+ "created": created_at,
100
+ "modified": modified_at,
101
+ "permalink": f"{project_dir}/docs/{doc_file}",
102
+ "project_name": project["name"],
103
+ "project_uuid": project["uuid"],
104
+ "doc_uuid": doc["uuid"],
105
+ }
106
+ ),
107
+ content=doc["content"],
108
+ )
109
+
110
+ return entity
111
+
112
+ def _format_prompt_markdown(self, project: Dict[str, Any]) -> Optional[EntityMarkdown]:
113
+ """Format project prompt template as a Basic Memory entity.
114
+
115
+ Args:
116
+ project: Project data.
117
+
118
+ Returns:
119
+ EntityMarkdown instance representing the prompt template, or None if
120
+ no prompt template exists.
121
+ """
122
+ if not project.get("prompt_template"):
123
+ return None
124
+
125
+ # Extract timestamps
126
+ created_at = project["created_at"]
127
+ modified_at = project["updated_at"]
128
+
129
+ # Generate clean project directory name
130
+ project_dir = clean_filename(project["name"])
131
+
132
+ # Create entity
133
+ entity = EntityMarkdown(
134
+ frontmatter=EntityFrontmatter(
135
+ metadata={
136
+ "type": "prompt_template",
137
+ "title": f"Prompt Template: {project['name']}",
138
+ "created": created_at,
139
+ "modified": modified_at,
140
+ "permalink": f"{project_dir}/prompt-template",
141
+ "project_name": project["name"],
142
+ "project_uuid": project["uuid"],
143
+ }
144
+ ),
145
+ content=f"# Prompt Template: {project['name']}\n\n{project['prompt_template']}",
146
+ )
147
+
148
+ return entity
@@ -0,0 +1,108 @@
1
+ """Memory JSON import service for Basic Memory."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List
5
+
6
+ from basic_memory.config import get_project_config
7
+ from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown, Observation, Relation
8
+ from basic_memory.importers.base import Importer
9
+ from basic_memory.schemas.importer import EntityImportResult
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class MemoryJsonImporter(Importer[EntityImportResult]):
15
+ """Service for importing memory.json format data."""
16
+
17
+ async def import_data(
18
+ self, source_data, destination_folder: str = "", **kwargs: Any
19
+ ) -> EntityImportResult:
20
+ """Import entities and relations from a memory.json file.
21
+
22
+ Args:
23
+ source_data: Path to the memory.json file.
24
+ destination_folder: Optional destination folder within the project.
25
+ **kwargs: Additional keyword arguments.
26
+
27
+ Returns:
28
+ EntityImportResult containing statistics and status of the import.
29
+ """
30
+ config = get_project_config()
31
+ try:
32
+ # First pass - collect all relations by source entity
33
+ entity_relations: Dict[str, List[Relation]] = {}
34
+ entities: Dict[str, Dict[str, Any]] = {}
35
+ skipped_entities: int = 0
36
+
37
+ # Ensure the base path exists
38
+ base_path = config.home # pragma: no cover
39
+ if destination_folder: # pragma: no cover
40
+ base_path = self.ensure_folder_exists(destination_folder)
41
+
42
+ # First pass - collect entities and relations
43
+ for line in source_data:
44
+ data = line
45
+ if data["type"] == "entity":
46
+ # Handle different possible name keys
47
+ entity_name = data.get("name") or data.get("entityName") or data.get("id")
48
+ if not entity_name:
49
+ logger.warning(f"Entity missing name field: {data}")
50
+ skipped_entities += 1
51
+ continue
52
+ entities[entity_name] = data
53
+ elif data["type"] == "relation":
54
+ # Store relation with its source entity
55
+ source = data.get("from") or data.get("from_id")
56
+ if source not in entity_relations:
57
+ entity_relations[source] = []
58
+ entity_relations[source].append(
59
+ Relation(
60
+ type=data.get("relationType") or data.get("relation_type"),
61
+ target=data.get("to") or data.get("to_id"),
62
+ )
63
+ )
64
+
65
+ # Second pass - create and write entities
66
+ entities_created = 0
67
+ for name, entity_data in entities.items():
68
+ # Get entity type with fallback
69
+ entity_type = entity_data.get("entityType") or entity_data.get("type") or "entity"
70
+
71
+ # Ensure entity type directory exists
72
+ entity_type_dir = base_path / entity_type
73
+ entity_type_dir.mkdir(parents=True, exist_ok=True)
74
+
75
+ # Get observations with fallback to empty list
76
+ observations = entity_data.get("observations", [])
77
+
78
+ entity = EntityMarkdown(
79
+ frontmatter=EntityFrontmatter(
80
+ metadata={
81
+ "type": entity_type,
82
+ "title": name,
83
+ "permalink": f"{entity_type}/{name}",
84
+ }
85
+ ),
86
+ content=f"# {name}\n",
87
+ observations=[Observation(content=obs) for obs in observations],
88
+ relations=entity_relations.get(name, []),
89
+ )
90
+
91
+ # Write entity file
92
+ file_path = base_path / f"{entity_type}/{name}.md"
93
+ await self.write_entity(entity, file_path)
94
+ entities_created += 1
95
+
96
+ relations_count = sum(len(rels) for rels in entity_relations.values())
97
+
98
+ return EntityImportResult(
99
+ import_count={"entities": entities_created, "relations": relations_count},
100
+ success=True,
101
+ entities=entities_created,
102
+ relations=relations_count,
103
+ skipped_entities=skipped_entities,
104
+ )
105
+
106
+ except Exception as e: # pragma: no cover
107
+ logger.exception("Failed to import memory.json")
108
+ return self.handle_error("Failed to import memory.json", e) # pyright: ignore [reportReturnType]