basic-memory 0.7.0__py3-none-any.whl → 0.17.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (195) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +130 -20
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  6. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  7. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
  8. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
  9. basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
  10. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  11. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  12. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  13. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  14. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
  15. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  16. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  17. basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
  18. basic_memory/api/app.py +87 -20
  19. basic_memory/api/container.py +133 -0
  20. basic_memory/api/routers/__init__.py +4 -1
  21. basic_memory/api/routers/directory_router.py +84 -0
  22. basic_memory/api/routers/importer_router.py +152 -0
  23. basic_memory/api/routers/knowledge_router.py +180 -23
  24. basic_memory/api/routers/management_router.py +80 -0
  25. basic_memory/api/routers/memory_router.py +9 -64
  26. basic_memory/api/routers/project_router.py +460 -0
  27. basic_memory/api/routers/prompt_router.py +260 -0
  28. basic_memory/api/routers/resource_router.py +136 -11
  29. basic_memory/api/routers/search_router.py +5 -5
  30. basic_memory/api/routers/utils.py +169 -0
  31. basic_memory/api/template_loader.py +292 -0
  32. basic_memory/api/v2/__init__.py +35 -0
  33. basic_memory/api/v2/routers/__init__.py +21 -0
  34. basic_memory/api/v2/routers/directory_router.py +93 -0
  35. basic_memory/api/v2/routers/importer_router.py +181 -0
  36. basic_memory/api/v2/routers/knowledge_router.py +427 -0
  37. basic_memory/api/v2/routers/memory_router.py +130 -0
  38. basic_memory/api/v2/routers/project_router.py +359 -0
  39. basic_memory/api/v2/routers/prompt_router.py +269 -0
  40. basic_memory/api/v2/routers/resource_router.py +286 -0
  41. basic_memory/api/v2/routers/search_router.py +73 -0
  42. basic_memory/cli/app.py +80 -10
  43. basic_memory/cli/auth.py +300 -0
  44. basic_memory/cli/commands/__init__.py +15 -2
  45. basic_memory/cli/commands/cloud/__init__.py +6 -0
  46. basic_memory/cli/commands/cloud/api_client.py +127 -0
  47. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  48. basic_memory/cli/commands/cloud/cloud_utils.py +108 -0
  49. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  50. basic_memory/cli/commands/cloud/rclone_commands.py +397 -0
  51. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  52. basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
  53. basic_memory/cli/commands/cloud/upload.py +240 -0
  54. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  55. basic_memory/cli/commands/command_utils.py +99 -0
  56. basic_memory/cli/commands/db.py +87 -12
  57. basic_memory/cli/commands/format.py +198 -0
  58. basic_memory/cli/commands/import_chatgpt.py +47 -223
  59. basic_memory/cli/commands/import_claude_conversations.py +48 -171
  60. basic_memory/cli/commands/import_claude_projects.py +53 -160
  61. basic_memory/cli/commands/import_memory_json.py +55 -111
  62. basic_memory/cli/commands/mcp.py +67 -11
  63. basic_memory/cli/commands/project.py +889 -0
  64. basic_memory/cli/commands/status.py +52 -34
  65. basic_memory/cli/commands/telemetry.py +81 -0
  66. basic_memory/cli/commands/tool.py +341 -0
  67. basic_memory/cli/container.py +84 -0
  68. basic_memory/cli/main.py +14 -6
  69. basic_memory/config.py +580 -26
  70. basic_memory/db.py +285 -28
  71. basic_memory/deps/__init__.py +293 -0
  72. basic_memory/deps/config.py +26 -0
  73. basic_memory/deps/db.py +56 -0
  74. basic_memory/deps/importers.py +200 -0
  75. basic_memory/deps/projects.py +238 -0
  76. basic_memory/deps/repositories.py +179 -0
  77. basic_memory/deps/services.py +480 -0
  78. basic_memory/deps.py +16 -185
  79. basic_memory/file_utils.py +318 -54
  80. basic_memory/ignore_utils.py +297 -0
  81. basic_memory/importers/__init__.py +27 -0
  82. basic_memory/importers/base.py +100 -0
  83. basic_memory/importers/chatgpt_importer.py +245 -0
  84. basic_memory/importers/claude_conversations_importer.py +192 -0
  85. basic_memory/importers/claude_projects_importer.py +184 -0
  86. basic_memory/importers/memory_json_importer.py +128 -0
  87. basic_memory/importers/utils.py +61 -0
  88. basic_memory/markdown/entity_parser.py +182 -23
  89. basic_memory/markdown/markdown_processor.py +70 -7
  90. basic_memory/markdown/plugins.py +43 -23
  91. basic_memory/markdown/schemas.py +1 -1
  92. basic_memory/markdown/utils.py +38 -14
  93. basic_memory/mcp/async_client.py +135 -4
  94. basic_memory/mcp/clients/__init__.py +28 -0
  95. basic_memory/mcp/clients/directory.py +70 -0
  96. basic_memory/mcp/clients/knowledge.py +176 -0
  97. basic_memory/mcp/clients/memory.py +120 -0
  98. basic_memory/mcp/clients/project.py +89 -0
  99. basic_memory/mcp/clients/resource.py +71 -0
  100. basic_memory/mcp/clients/search.py +65 -0
  101. basic_memory/mcp/container.py +110 -0
  102. basic_memory/mcp/project_context.py +155 -0
  103. basic_memory/mcp/prompts/__init__.py +19 -0
  104. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  105. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  106. basic_memory/mcp/prompts/recent_activity.py +188 -0
  107. basic_memory/mcp/prompts/search.py +57 -0
  108. basic_memory/mcp/prompts/utils.py +162 -0
  109. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  110. basic_memory/mcp/resources/project_info.py +71 -0
  111. basic_memory/mcp/server.py +61 -9
  112. basic_memory/mcp/tools/__init__.py +33 -21
  113. basic_memory/mcp/tools/build_context.py +120 -0
  114. basic_memory/mcp/tools/canvas.py +152 -0
  115. basic_memory/mcp/tools/chatgpt_tools.py +190 -0
  116. basic_memory/mcp/tools/delete_note.py +249 -0
  117. basic_memory/mcp/tools/edit_note.py +325 -0
  118. basic_memory/mcp/tools/list_directory.py +157 -0
  119. basic_memory/mcp/tools/move_note.py +549 -0
  120. basic_memory/mcp/tools/project_management.py +204 -0
  121. basic_memory/mcp/tools/read_content.py +281 -0
  122. basic_memory/mcp/tools/read_note.py +265 -0
  123. basic_memory/mcp/tools/recent_activity.py +528 -0
  124. basic_memory/mcp/tools/search.py +377 -24
  125. basic_memory/mcp/tools/utils.py +402 -16
  126. basic_memory/mcp/tools/view_note.py +78 -0
  127. basic_memory/mcp/tools/write_note.py +230 -0
  128. basic_memory/models/__init__.py +3 -2
  129. basic_memory/models/knowledge.py +82 -17
  130. basic_memory/models/project.py +93 -0
  131. basic_memory/models/search.py +68 -8
  132. basic_memory/project_resolver.py +222 -0
  133. basic_memory/repository/__init__.py +2 -0
  134. basic_memory/repository/entity_repository.py +437 -8
  135. basic_memory/repository/observation_repository.py +36 -3
  136. basic_memory/repository/postgres_search_repository.py +451 -0
  137. basic_memory/repository/project_info_repository.py +10 -0
  138. basic_memory/repository/project_repository.py +140 -0
  139. basic_memory/repository/relation_repository.py +79 -4
  140. basic_memory/repository/repository.py +148 -29
  141. basic_memory/repository/search_index_row.py +95 -0
  142. basic_memory/repository/search_repository.py +79 -268
  143. basic_memory/repository/search_repository_base.py +241 -0
  144. basic_memory/repository/sqlite_search_repository.py +437 -0
  145. basic_memory/runtime.py +61 -0
  146. basic_memory/schemas/__init__.py +22 -9
  147. basic_memory/schemas/base.py +131 -12
  148. basic_memory/schemas/cloud.py +50 -0
  149. basic_memory/schemas/directory.py +31 -0
  150. basic_memory/schemas/importer.py +35 -0
  151. basic_memory/schemas/memory.py +194 -25
  152. basic_memory/schemas/project_info.py +213 -0
  153. basic_memory/schemas/prompt.py +90 -0
  154. basic_memory/schemas/request.py +56 -2
  155. basic_memory/schemas/response.py +85 -28
  156. basic_memory/schemas/search.py +36 -35
  157. basic_memory/schemas/sync_report.py +72 -0
  158. basic_memory/schemas/v2/__init__.py +27 -0
  159. basic_memory/schemas/v2/entity.py +133 -0
  160. basic_memory/schemas/v2/resource.py +47 -0
  161. basic_memory/services/__init__.py +2 -1
  162. basic_memory/services/context_service.py +451 -138
  163. basic_memory/services/directory_service.py +310 -0
  164. basic_memory/services/entity_service.py +636 -71
  165. basic_memory/services/exceptions.py +21 -0
  166. basic_memory/services/file_service.py +402 -33
  167. basic_memory/services/initialization.py +216 -0
  168. basic_memory/services/link_resolver.py +50 -56
  169. basic_memory/services/project_service.py +888 -0
  170. basic_memory/services/search_service.py +232 -37
  171. basic_memory/sync/__init__.py +4 -2
  172. basic_memory/sync/background_sync.py +26 -0
  173. basic_memory/sync/coordinator.py +160 -0
  174. basic_memory/sync/sync_service.py +1200 -109
  175. basic_memory/sync/watch_service.py +432 -135
  176. basic_memory/telemetry.py +249 -0
  177. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  178. basic_memory/templates/prompts/search.hbs +101 -0
  179. basic_memory/utils.py +407 -54
  180. basic_memory-0.17.4.dist-info/METADATA +617 -0
  181. basic_memory-0.17.4.dist-info/RECORD +193 -0
  182. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
  183. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +1 -0
  184. basic_memory/alembic/README +0 -1
  185. basic_memory/cli/commands/sync.py +0 -206
  186. basic_memory/cli/commands/tools.py +0 -157
  187. basic_memory/mcp/tools/knowledge.py +0 -68
  188. basic_memory/mcp/tools/memory.py +0 -170
  189. basic_memory/mcp/tools/notes.py +0 -202
  190. basic_memory/schemas/discovery.py +0 -28
  191. basic_memory/sync/file_change_scanner.py +0 -158
  192. basic_memory/sync/utils.py +0 -31
  193. basic_memory-0.7.0.dist-info/METADATA +0 -378
  194. basic_memory-0.7.0.dist-info/RECORD +0 -82
  195. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,192 @@
1
+ """Claude conversations import service for Basic Memory."""
2
+
3
+ import logging
4
+ from datetime import datetime
5
+ from typing import Any, Dict, List, Optional
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 ClaudeConversationsImporter(Importer[ChatImportResult]):
16
+ """Service for importing Claude conversations."""
17
+
18
+ def handle_error( # pragma: no cover
19
+ self, message: str, error: Optional[Exception] = None
20
+ ) -> ChatImportResult:
21
+ """Return a failed ChatImportResult with an error message."""
22
+ error_msg = f"{message}: {error}" if error else message
23
+ return ChatImportResult(
24
+ import_count={},
25
+ success=False,
26
+ error_message=error_msg,
27
+ conversations=0,
28
+ messages=0,
29
+ )
30
+
31
+ async def import_data(
32
+ self, source_data, destination_folder: str, **kwargs: Any
33
+ ) -> ChatImportResult:
34
+ """Import conversations from Claude JSON export.
35
+
36
+ Args:
37
+ source_data: Path to the Claude conversations.json file.
38
+ destination_folder: Destination folder within the project.
39
+ **kwargs: Additional keyword arguments.
40
+
41
+ Returns:
42
+ ChatImportResult containing statistics and status of the import.
43
+ """
44
+ try:
45
+ # Ensure the destination folder exists
46
+ await self.ensure_folder_exists(destination_folder)
47
+
48
+ conversations = source_data
49
+
50
+ # Process each conversation
51
+ messages_imported = 0
52
+ chats_imported = 0
53
+
54
+ for chat in conversations:
55
+ # Get name, providing default for unnamed conversations
56
+ chat_name = chat.get("name") or f"Conversation {chat.get('uuid', 'untitled')}"
57
+
58
+ # Convert to entity
59
+ entity = self._format_chat_content(
60
+ folder=destination_folder,
61
+ name=chat_name,
62
+ messages=chat["chat_messages"],
63
+ created_at=chat["created_at"],
64
+ modified_at=chat["updated_at"],
65
+ )
66
+
67
+ # Write file using relative path - FileService handles base_path
68
+ file_path = f"{entity.frontmatter.metadata['permalink']}.md"
69
+ await self.write_entity(entity, file_path)
70
+
71
+ chats_imported += 1
72
+ messages_imported += len(chat["chat_messages"])
73
+
74
+ return ChatImportResult(
75
+ import_count={"conversations": chats_imported, "messages": messages_imported},
76
+ success=True,
77
+ conversations=chats_imported,
78
+ messages=messages_imported,
79
+ )
80
+
81
+ except Exception as e: # pragma: no cover
82
+ logger.exception("Failed to import Claude conversations")
83
+ return self.handle_error("Failed to import Claude conversations", e)
84
+
85
+ def _format_chat_content(
86
+ self,
87
+ folder: str,
88
+ name: str,
89
+ messages: List[Dict[str, Any]],
90
+ created_at: str,
91
+ modified_at: str,
92
+ ) -> EntityMarkdown:
93
+ """Convert chat messages to Basic Memory entity format.
94
+
95
+ Args:
96
+ folder: Destination folder name (relative path).
97
+ name: Chat name.
98
+ messages: List of chat messages.
99
+ created_at: Creation timestamp.
100
+ modified_at: Modification timestamp.
101
+
102
+ Returns:
103
+ EntityMarkdown instance representing the conversation.
104
+ """
105
+ # Generate permalink using folder name (relative path)
106
+ date_prefix = datetime.fromisoformat(created_at.replace("Z", "+00:00")).strftime("%Y%m%d")
107
+ clean_title = clean_filename(name)
108
+ permalink = f"{folder}/{date_prefix}-{clean_title}"
109
+
110
+ # Format content
111
+ content = self._format_chat_markdown(
112
+ name=name,
113
+ messages=messages,
114
+ created_at=created_at,
115
+ modified_at=modified_at,
116
+ permalink=permalink,
117
+ )
118
+
119
+ # Create entity
120
+ entity = EntityMarkdown(
121
+ frontmatter=EntityFrontmatter(
122
+ metadata={
123
+ "type": "conversation",
124
+ "title": name,
125
+ "created": created_at,
126
+ "modified": modified_at,
127
+ "permalink": permalink,
128
+ }
129
+ ),
130
+ content=content,
131
+ )
132
+
133
+ return entity
134
+
135
+ def _format_chat_markdown(
136
+ self,
137
+ name: str,
138
+ messages: List[Dict[str, Any]],
139
+ created_at: str,
140
+ modified_at: str,
141
+ permalink: str,
142
+ ) -> str:
143
+ """Format chat as clean markdown.
144
+
145
+ Args:
146
+ name: Chat name.
147
+ messages: List of chat messages.
148
+ created_at: Creation timestamp.
149
+ modified_at: Modification timestamp.
150
+ permalink: Permalink for the entity.
151
+
152
+ Returns:
153
+ Formatted markdown content.
154
+ """
155
+ # Start with frontmatter and title
156
+ lines = [
157
+ f"# {name}\n",
158
+ ]
159
+
160
+ # Add messages
161
+ for msg in messages:
162
+ # Format timestamp
163
+ ts = format_timestamp(msg["created_at"])
164
+
165
+ # Add message header
166
+ lines.append(f"### {msg['sender'].title()} ({ts})")
167
+
168
+ # Handle message content
169
+ content = msg.get("text", "")
170
+ if msg.get("content"):
171
+ # Filter out None values before joining
172
+ content = " ".join(
173
+ str(c.get("text", ""))
174
+ for c in msg["content"]
175
+ if c and c.get("text") is not None
176
+ )
177
+ lines.append(content)
178
+
179
+ # Handle attachments
180
+ attachments = msg.get("attachments", [])
181
+ for attachment in attachments:
182
+ if "file_name" in attachment:
183
+ lines.append(f"\n**Attachment: {attachment['file_name']}**")
184
+ if "extracted_content" in attachment:
185
+ lines.append("```")
186
+ lines.append(attachment["extracted_content"])
187
+ lines.append("```")
188
+
189
+ # Add spacing between messages
190
+ lines.append("")
191
+
192
+ return "\n".join(lines)
@@ -0,0 +1,184 @@
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
+ def handle_error( # pragma: no cover
18
+ self, message: str, error: Optional[Exception] = None
19
+ ) -> ProjectImportResult:
20
+ """Return a failed ProjectImportResult with an error message."""
21
+ error_msg = f"{message}: {error}" if error else message
22
+ return ProjectImportResult(
23
+ import_count={},
24
+ success=False,
25
+ error_message=error_msg,
26
+ documents=0,
27
+ prompts=0,
28
+ )
29
+
30
+ async def import_data(
31
+ self, source_data, destination_folder: str, **kwargs: Any
32
+ ) -> ProjectImportResult:
33
+ """Import projects from Claude JSON export.
34
+
35
+ Args:
36
+ source_path: Path to the Claude projects.json file.
37
+ destination_folder: Base folder for projects within the project.
38
+ **kwargs: Additional keyword arguments.
39
+
40
+ Returns:
41
+ ProjectImportResult containing statistics and status of the import.
42
+ """
43
+ try:
44
+ # Ensure the base folder exists
45
+ if destination_folder:
46
+ await self.ensure_folder_exists(destination_folder)
47
+
48
+ projects = source_data
49
+
50
+ # Process each project
51
+ docs_imported = 0
52
+ prompts_imported = 0
53
+
54
+ for project in projects:
55
+ project_dir = clean_filename(project["name"])
56
+
57
+ # Create project directories using FileService with relative path
58
+ docs_dir = (
59
+ f"{destination_folder}/{project_dir}/docs"
60
+ if destination_folder
61
+ else f"{project_dir}/docs"
62
+ )
63
+ await self.file_service.ensure_directory(docs_dir)
64
+
65
+ # Import prompt template if it exists
66
+ if prompt_entity := self._format_prompt_markdown(project, destination_folder):
67
+ # Write file using relative path - FileService handles base_path
68
+ file_path = f"{prompt_entity.frontmatter.metadata['permalink']}.md"
69
+ await self.write_entity(prompt_entity, file_path)
70
+ prompts_imported += 1
71
+
72
+ # Import project documents
73
+ for doc in project.get("docs", []):
74
+ entity = self._format_project_markdown(project, doc, destination_folder)
75
+ # Write file using relative path - FileService handles base_path
76
+ file_path = f"{entity.frontmatter.metadata['permalink']}.md"
77
+ await self.write_entity(entity, file_path)
78
+ docs_imported += 1
79
+
80
+ return ProjectImportResult(
81
+ import_count={"documents": docs_imported, "prompts": prompts_imported},
82
+ success=True,
83
+ documents=docs_imported,
84
+ prompts=prompts_imported,
85
+ )
86
+
87
+ except Exception as e: # pragma: no cover
88
+ logger.exception("Failed to import Claude projects")
89
+ return self.handle_error("Failed to import Claude projects", e)
90
+
91
+ def _format_project_markdown(
92
+ self, project: Dict[str, Any], doc: Dict[str, Any], destination_folder: str = ""
93
+ ) -> EntityMarkdown:
94
+ """Format a project document as a Basic Memory entity.
95
+
96
+ Args:
97
+ project: Project data.
98
+ doc: Document data.
99
+ destination_folder: Optional destination folder prefix.
100
+
101
+ Returns:
102
+ EntityMarkdown instance representing the document.
103
+ """
104
+ # Extract timestamps
105
+ created_at = doc.get("created_at") or project["created_at"]
106
+ modified_at = project["updated_at"]
107
+
108
+ # Generate clean names for organization
109
+ project_dir = clean_filename(project["name"])
110
+ doc_file = clean_filename(doc["filename"])
111
+
112
+ # Build permalink with optional destination folder prefix
113
+ permalink = (
114
+ f"{destination_folder}/{project_dir}/docs/{doc_file}"
115
+ if destination_folder
116
+ else f"{project_dir}/docs/{doc_file}"
117
+ )
118
+
119
+ # Create entity
120
+ entity = EntityMarkdown(
121
+ frontmatter=EntityFrontmatter(
122
+ metadata={
123
+ "type": "project_doc",
124
+ "title": doc["filename"],
125
+ "created": created_at,
126
+ "modified": modified_at,
127
+ "permalink": permalink,
128
+ "project_name": project["name"],
129
+ "project_uuid": project["uuid"],
130
+ "doc_uuid": doc["uuid"],
131
+ }
132
+ ),
133
+ content=doc["content"],
134
+ )
135
+
136
+ return entity
137
+
138
+ def _format_prompt_markdown(
139
+ self, project: Dict[str, Any], destination_folder: str = ""
140
+ ) -> Optional[EntityMarkdown]:
141
+ """Format project prompt template as a Basic Memory entity.
142
+
143
+ Args:
144
+ project: Project data.
145
+ destination_folder: Optional destination folder prefix.
146
+
147
+ Returns:
148
+ EntityMarkdown instance representing the prompt template, or None if
149
+ no prompt template exists.
150
+ """
151
+ if not project.get("prompt_template"):
152
+ return None
153
+
154
+ # Extract timestamps
155
+ created_at = project["created_at"]
156
+ modified_at = project["updated_at"]
157
+
158
+ # Generate clean project directory name
159
+ project_dir = clean_filename(project["name"])
160
+
161
+ # Build permalink with optional destination folder prefix
162
+ permalink = (
163
+ f"{destination_folder}/{project_dir}/prompt-template"
164
+ if destination_folder
165
+ else f"{project_dir}/prompt-template"
166
+ )
167
+
168
+ # Create entity
169
+ entity = EntityMarkdown(
170
+ frontmatter=EntityFrontmatter(
171
+ metadata={
172
+ "type": "prompt_template",
173
+ "title": f"Prompt Template: {project['name']}",
174
+ "created": created_at,
175
+ "modified": modified_at,
176
+ "permalink": permalink,
177
+ "project_name": project["name"],
178
+ "project_uuid": project["uuid"],
179
+ }
180
+ ),
181
+ content=f"# Prompt Template: {project['name']}\n\n{project['prompt_template']}",
182
+ )
183
+
184
+ return entity
@@ -0,0 +1,128 @@
1
+ """Memory JSON import service for Basic Memory."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown, Observation, Relation
7
+ from basic_memory.importers.base import Importer
8
+ from basic_memory.schemas.importer import EntityImportResult
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class MemoryJsonImporter(Importer[EntityImportResult]):
14
+ """Service for importing memory.json format data."""
15
+
16
+ def handle_error( # pragma: no cover
17
+ self, message: str, error: Optional[Exception] = None
18
+ ) -> EntityImportResult:
19
+ """Return a failed EntityImportResult with an error message."""
20
+ error_msg = f"{message}: {error}" if error else message
21
+ return EntityImportResult(
22
+ import_count={},
23
+ success=False,
24
+ error_message=error_msg,
25
+ entities=0,
26
+ relations=0,
27
+ skipped_entities=0,
28
+ )
29
+
30
+ async def import_data(
31
+ self, source_data, destination_folder: str = "", **kwargs: Any
32
+ ) -> EntityImportResult:
33
+ """Import entities and relations from a memory.json file.
34
+
35
+ Args:
36
+ source_data: Path to the memory.json file.
37
+ destination_folder: Optional destination folder within the project.
38
+ **kwargs: Additional keyword arguments.
39
+
40
+ Returns:
41
+ EntityImportResult containing statistics and status of the import.
42
+ """
43
+ try:
44
+ # First pass - collect all relations by source entity
45
+ entity_relations: Dict[str, List[Relation]] = {}
46
+ entities: Dict[str, Dict[str, Any]] = {}
47
+ skipped_entities: int = 0
48
+
49
+ # Ensure the destination folder exists if provided
50
+ if destination_folder: # pragma: no cover
51
+ await self.ensure_folder_exists(destination_folder)
52
+
53
+ # First pass - collect entities and relations
54
+ for line in source_data:
55
+ data = line
56
+ if data["type"] == "entity":
57
+ # Handle different possible name keys
58
+ entity_name = data.get("name") or data.get("entityName") or data.get("id")
59
+ if not entity_name:
60
+ logger.warning(f"Entity missing name field: {data}") # pragma: no cover
61
+ skipped_entities += 1 # pragma: no cover
62
+ continue # pragma: no cover
63
+ entities[entity_name] = data
64
+ elif data["type"] == "relation":
65
+ # Store relation with its source entity
66
+ source = data.get("from") or data.get("from_id")
67
+ if source not in entity_relations:
68
+ entity_relations[source] = []
69
+ entity_relations[source].append(
70
+ Relation(
71
+ type=data.get("relationType") or data.get("relation_type"),
72
+ target=data.get("to") or data.get("to_id"),
73
+ )
74
+ )
75
+
76
+ # Second pass - create and write entities
77
+ entities_created = 0
78
+ for name, entity_data in entities.items():
79
+ # Get entity type with fallback
80
+ entity_type = entity_data.get("entityType") or entity_data.get("type") or "entity"
81
+
82
+ # Build permalink with optional destination folder prefix
83
+ permalink = (
84
+ f"{destination_folder}/{entity_type}/{name}"
85
+ if destination_folder
86
+ else f"{entity_type}/{name}"
87
+ )
88
+
89
+ # Ensure entity type directory exists using FileService with relative path
90
+ entity_type_dir = (
91
+ f"{destination_folder}/{entity_type}" if destination_folder else entity_type
92
+ )
93
+ await self.file_service.ensure_directory(entity_type_dir)
94
+
95
+ # Get observations with fallback to empty list
96
+ observations = entity_data.get("observations", [])
97
+
98
+ entity = EntityMarkdown(
99
+ frontmatter=EntityFrontmatter(
100
+ metadata={
101
+ "type": entity_type,
102
+ "title": name,
103
+ "permalink": permalink,
104
+ }
105
+ ),
106
+ content=f"# {name}\n",
107
+ observations=[Observation(content=obs) for obs in observations],
108
+ relations=entity_relations.get(name, []),
109
+ )
110
+
111
+ # Write file using relative path - FileService handles base_path
112
+ file_path = f"{entity.frontmatter.metadata['permalink']}.md"
113
+ await self.write_entity(entity, file_path)
114
+ entities_created += 1
115
+
116
+ relations_count = sum(len(rels) for rels in entity_relations.values())
117
+
118
+ return EntityImportResult(
119
+ import_count={"entities": entities_created, "relations": relations_count},
120
+ success=True,
121
+ entities=entities_created,
122
+ relations=relations_count,
123
+ skipped_entities=skipped_entities,
124
+ )
125
+
126
+ except Exception as e: # pragma: no cover
127
+ logger.exception("Failed to import memory.json")
128
+ return self.handle_error("Failed to import memory.json", e)
@@ -0,0 +1,61 @@
1
+ """Utility functions for import services."""
2
+
3
+ import re
4
+ from datetime import datetime
5
+ from typing import Any
6
+
7
+
8
+ def clean_filename(name: str | None) -> str: # pragma: no cover
9
+ """Clean a string to be used as a filename.
10
+
11
+ Args:
12
+ name: The string to clean (can be None).
13
+
14
+ Returns:
15
+ A cleaned string suitable for use as a filename.
16
+ """
17
+ # Handle None or empty input
18
+ if not name:
19
+ return "untitled"
20
+ # Replace common punctuation and whitespace with underscores
21
+ name = re.sub(r"[\s\-,.:/\\\[\]\(\)]+", "_", name)
22
+ # Remove any non-alphanumeric or underscore characters
23
+ name = re.sub(r"[^\w]+", "", name)
24
+ # Ensure the name isn't too long
25
+ if len(name) > 100: # pragma: no cover
26
+ name = name[:100]
27
+ # Ensure the name isn't empty
28
+ if not name: # pragma: no cover
29
+ name = "untitled"
30
+ return name
31
+
32
+
33
+ def format_timestamp(timestamp: Any) -> str: # pragma: no cover
34
+ """Format a timestamp for use in a filename or title.
35
+
36
+ Args:
37
+ timestamp: A timestamp in various formats.
38
+
39
+ Returns:
40
+ A formatted string representation of the timestamp.
41
+ """
42
+ if isinstance(timestamp, str):
43
+ try:
44
+ # Try ISO format
45
+ timestamp = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
46
+ except ValueError:
47
+ try:
48
+ # Try unix timestamp as string
49
+ timestamp = datetime.fromtimestamp(float(timestamp)).astimezone()
50
+ except ValueError:
51
+ # Return as is if we can't parse it
52
+ return timestamp
53
+ elif isinstance(timestamp, (int, float)):
54
+ # Unix timestamp
55
+ timestamp = datetime.fromtimestamp(timestamp).astimezone()
56
+
57
+ if isinstance(timestamp, datetime):
58
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
59
+
60
+ # Return as is if we can't format it
61
+ return str(timestamp) # pragma: no cover