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
@@ -2,209 +2,30 @@
2
2
 
3
3
  import asyncio
4
4
  import json
5
- from datetime import datetime
6
5
  from pathlib import Path
7
- from typing import Dict, Any, List, Annotated, Set, Optional
6
+ from typing import Annotated, Tuple
8
7
 
9
- import logfire
10
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 basic_memory.services.file_service import FileService
11
14
  from loguru import logger
12
15
  from rich.console import Console
13
16
  from rich.panel import Panel
14
- from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
15
-
16
- from basic_memory.cli.app import import_app
17
- from basic_memory.config import config
18
- from basic_memory.markdown import EntityParser, MarkdownProcessor
19
- from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
20
17
 
21
18
  console = Console()
22
19
 
23
20
 
24
- def clean_filename(text: str) -> str:
25
- """Convert text to safe filename."""
26
- clean = "".join(c if c.isalnum() else "-" for c in text.lower()).strip("-")
27
- return clean
28
-
29
-
30
- def format_timestamp(ts: float) -> str:
31
- """Format Unix timestamp for display."""
32
- dt = datetime.fromtimestamp(ts)
33
- return dt.strftime("%Y-%m-%d %H:%M:%S")
34
-
35
-
36
- def get_message_content(message: Dict[str, Any]) -> str:
37
- """Extract clean message content."""
38
- if not message or "content" not in message:
39
- return "" # pragma: no cover
40
-
41
- content = message["content"]
42
- if content.get("content_type") == "text":
43
- return "\n".join(content.get("parts", []))
44
- elif content.get("content_type") == "code":
45
- return f"```{content.get('language', '')}\n{content.get('text', '')}\n```"
46
- return "" # pragma: no cover
47
-
48
-
49
- def traverse_messages(
50
- mapping: Dict[str, Any], root_id: Optional[str], seen: Set[str]
51
- ) -> List[Dict[str, Any]]:
52
- """Traverse message tree and return messages in order."""
53
- messages = []
54
- node = mapping.get(root_id) if root_id else None
55
-
56
- while node:
57
- if node["id"] not in seen and node.get("message"):
58
- seen.add(node["id"])
59
- messages.append(node["message"])
60
-
61
- # Follow children
62
- children = node.get("children", [])
63
- for child_id in children:
64
- child_msgs = traverse_messages(mapping, child_id, seen)
65
- messages.extend(child_msgs)
66
-
67
- break # Don't follow siblings
68
-
69
- return messages
70
-
71
-
72
- def format_chat_markdown(
73
- title: str,
74
- mapping: Dict[str, Any],
75
- root_id: Optional[str],
76
- created_at: float,
77
- modified_at: float,
78
- ) -> str:
79
- """Format chat as clean markdown."""
80
-
81
- # Start with title
82
- lines = [f"# {title}\n"]
83
-
84
- # Traverse message tree
85
- seen_msgs = set()
86
- messages = traverse_messages(mapping, root_id, seen_msgs)
87
-
88
- # Format each message
89
- for msg in messages:
90
- # Skip hidden messages
91
- if msg.get("metadata", {}).get("is_visually_hidden_from_conversation"):
92
- continue
93
-
94
- # Get author and timestamp
95
- author = msg["author"]["role"].title()
96
- ts = format_timestamp(msg["create_time"]) if msg.get("create_time") else ""
97
-
98
- # Add message header
99
- lines.append(f"### {author} ({ts})")
100
-
101
- # Add message content
102
- content = get_message_content(msg)
103
- if content:
104
- lines.append(content)
105
-
106
- # Add spacing
107
- lines.append("")
108
-
109
- return "\n".join(lines)
110
-
111
-
112
- def format_chat_content(folder: str, conversation: Dict[str, Any]) -> EntityMarkdown:
113
- """Convert chat conversation to Basic Memory entity."""
114
-
115
- # Extract timestamps
116
- created_at = conversation["create_time"]
117
- modified_at = conversation["update_time"]
118
-
119
- root_id = None
120
- # Find root message
121
- for node_id, node in conversation["mapping"].items():
122
- if node.get("parent") is None:
123
- root_id = node_id
124
- break
125
-
126
- # Generate permalink
127
- date_prefix = datetime.fromtimestamp(created_at).strftime("%Y%m%d")
128
- clean_title = clean_filename(conversation["title"])
129
-
130
- # Format content
131
- content = format_chat_markdown(
132
- title=conversation["title"],
133
- mapping=conversation["mapping"],
134
- root_id=root_id,
135
- created_at=created_at,
136
- modified_at=modified_at,
137
- )
138
-
139
- # Create entity
140
- entity = EntityMarkdown(
141
- frontmatter=EntityFrontmatter(
142
- metadata={
143
- "type": "conversation",
144
- "title": conversation["title"],
145
- "created": format_timestamp(created_at),
146
- "modified": format_timestamp(modified_at),
147
- "permalink": f"{folder}/{date_prefix}-{clean_title}",
148
- }
149
- ),
150
- content=content,
151
- )
152
-
153
- return entity
154
-
155
-
156
- async def process_chatgpt_json(
157
- json_path: Path, folder: str, markdown_processor: MarkdownProcessor
158
- ) -> Dict[str, int]:
159
- """Import conversations from ChatGPT JSON format."""
160
-
161
- with Progress(
162
- SpinnerColumn(),
163
- TextColumn("[progress.description]{task.description}"),
164
- BarColumn(),
165
- TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
166
- console=console,
167
- ) as progress:
168
- read_task = progress.add_task("Reading chat data...", total=None)
169
-
170
- # Read conversations
171
- conversations = json.loads(json_path.read_text())
172
- progress.update(read_task, total=len(conversations))
173
-
174
- # Process each conversation
175
- messages_imported = 0
176
- chats_imported = 0
177
-
178
- for chat in conversations:
179
- # Convert to entity
180
- entity = format_chat_content(folder, chat)
181
-
182
- # Write file
183
- file_path = config.home / f"{entity.frontmatter.metadata['permalink']}.md"
184
- # logger.info(f"Writing file: {file_path.absolute()}")
185
- await markdown_processor.write_file(file_path, entity)
186
-
187
- # Count messages
188
- msg_count = sum(
189
- 1
190
- for node in chat["mapping"].values()
191
- if node.get("message")
192
- and not node.get("message", {})
193
- .get("metadata", {})
194
- .get("is_visually_hidden_from_conversation")
195
- )
196
-
197
- chats_imported += 1
198
- messages_imported += msg_count
199
- progress.update(read_task, advance=1)
200
-
201
- return {"conversations": chats_imported, "messages": messages_imported}
202
-
203
-
204
- async def get_markdown_processor() -> MarkdownProcessor:
205
- """Get MarkdownProcessor instance."""
21
+ async def get_importer_dependencies() -> Tuple[MarkdownProcessor, FileService]:
22
+ """Get MarkdownProcessor and FileService instances for importers."""
23
+ config = get_project_config()
24
+ app_config = ConfigManager().config
206
25
  entity_parser = EntityParser(config.home)
207
- return MarkdownProcessor(entity_parser)
26
+ markdown_processor = MarkdownProcessor(entity_parser, app_config=app_config)
27
+ file_service = FileService(config.home, markdown_processor, app_config=app_config)
28
+ return markdown_processor, file_service
208
29
 
209
30
 
210
31
  @import_app.command(name="chatgpt", help="Import conversations from ChatGPT JSON export.")
@@ -226,38 +47,41 @@ def import_chatgpt(
226
47
  After importing, run 'basic-memory sync' to index the new files.
227
48
  """
228
49
 
229
- with logfire.span("import chatgpt"): # pyright: ignore [reportGeneralTypeIssues]
230
- try:
231
- if conversations_json:
232
- if not conversations_json.exists():
233
- typer.echo(f"Error: File not found: {conversations_json}", err=True)
234
- raise typer.Exit(1)
235
-
236
- # Get markdown processor
237
- markdown_processor = asyncio.run(get_markdown_processor())
50
+ try:
51
+ if not conversations_json.exists(): # pragma: no cover
52
+ typer.echo(f"Error: File not found: {conversations_json}", err=True)
53
+ raise typer.Exit(1)
238
54
 
239
- # Process the file
240
- base_path = config.home / folder
241
- console.print(
242
- f"\nImporting chats from {conversations_json}...writing to {base_path}"
243
- )
244
- results = asyncio.run(
245
- process_chatgpt_json(conversations_json, folder, markdown_processor)
246
- )
55
+ # Get importer dependencies
56
+ markdown_processor, file_service = asyncio.run(get_importer_dependencies())
57
+ config = get_project_config()
58
+ # Process the file
59
+ base_path = config.home / folder
60
+ console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
61
+
62
+ # Create importer and run import
63
+ importer = ChatGPTImporter(config.home, markdown_processor, file_service)
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)
247
71
 
248
- # Show results
249
- console.print(
250
- Panel(
251
- f"[green]Import complete![/green]\n\n"
252
- f"Imported {results['conversations']} conversations\n"
253
- f"Containing {results['messages']} messages",
254
- expand=False,
255
- )
256
- )
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
+ )
257
81
 
258
- console.print("\nRun 'basic-memory sync' to index the new files.")
82
+ console.print("\nRun 'basic-memory sync' to index the new files.")
259
83
 
260
- except Exception as e:
261
- logger.error("Import failed")
262
- typer.echo(f"Error during import: {e}", err=True)
263
- raise typer.Exit(1)
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)
@@ -2,162 +2,30 @@
2
2
 
3
3
  import asyncio
4
4
  import json
5
- from datetime import datetime
6
5
  from pathlib import Path
7
- from typing import Dict, Any, List, Annotated
6
+ from typing import Annotated, Tuple
8
7
 
9
- import logfire
10
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 basic_memory.services.file_service import FileService
11
14
  from loguru import logger
12
15
  from rich.console import Console
13
16
  from rich.panel import Panel
14
- from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
15
-
16
- from basic_memory.cli.app import claude_app
17
- from basic_memory.config import config
18
- from basic_memory.markdown import EntityParser, MarkdownProcessor
19
- from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
20
17
 
21
18
  console = Console()
22
19
 
23
20
 
24
- def clean_filename(text: str) -> str:
25
- """Convert text to safe filename."""
26
- # Remove invalid characters and convert spaces
27
- clean = "".join(c if c.isalnum() else "-" for c in text.lower()).strip("-")
28
- return clean
29
-
30
-
31
- def format_timestamp(ts: str) -> str:
32
- """Format ISO timestamp for display."""
33
- dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
34
- return dt.strftime("%Y-%m-%d %H:%M:%S")
35
-
36
-
37
- def format_chat_markdown(
38
- name: str, messages: List[Dict[str, Any]], created_at: str, modified_at: str, permalink: str
39
- ) -> str:
40
- """Format chat as clean markdown."""
41
-
42
- # Start with frontmatter and title
43
- lines = [
44
- f"# {name}\n",
45
- ]
46
-
47
- # Add messages
48
- for msg in messages:
49
- # Format timestamp
50
- ts = format_timestamp(msg["created_at"])
51
-
52
- # Add message header
53
- lines.append(f"### {msg['sender'].title()} ({ts})")
54
-
55
- # Handle message content
56
- content = msg.get("text", "")
57
- if msg.get("content"):
58
- content = " ".join(c.get("text", "") for c in msg["content"])
59
- lines.append(content)
60
-
61
- # Handle attachments
62
- attachments = msg.get("attachments", [])
63
- for attachment in attachments:
64
- if "file_name" in attachment:
65
- lines.append(f"\n**Attachment: {attachment['file_name']}**")
66
- if "extracted_content" in attachment:
67
- lines.append("```")
68
- lines.append(attachment["extracted_content"])
69
- lines.append("```")
70
-
71
- # Add spacing between messages
72
- lines.append("")
73
-
74
- return "\n".join(lines)
75
-
76
-
77
- def format_chat_content(
78
- base_path: Path, name: str, messages: List[Dict[str, Any]], created_at: str, modified_at: str
79
- ) -> EntityMarkdown:
80
- """Convert chat messages to Basic Memory entity format."""
81
-
82
- # Generate permalink
83
- date_prefix = datetime.fromisoformat(created_at.replace("Z", "+00:00")).strftime("%Y%m%d")
84
- clean_title = clean_filename(name)
85
- permalink = f"{base_path}/{date_prefix}-{clean_title}"
86
-
87
- # Format content
88
- content = format_chat_markdown(
89
- name=name,
90
- messages=messages,
91
- created_at=created_at,
92
- modified_at=modified_at,
93
- permalink=permalink,
94
- )
95
-
96
- # Create entity
97
- entity = EntityMarkdown(
98
- frontmatter=EntityFrontmatter(
99
- metadata={
100
- "type": "conversation",
101
- "title": name,
102
- "created": created_at,
103
- "modified": modified_at,
104
- "permalink": permalink,
105
- }
106
- ),
107
- content=content,
108
- )
109
-
110
- return entity
111
-
112
-
113
- async def process_conversations_json(
114
- json_path: Path, base_path: Path, markdown_processor: MarkdownProcessor
115
- ) -> Dict[str, int]:
116
- """Import chat data from conversations2.json format."""
117
-
118
- with Progress(
119
- SpinnerColumn(),
120
- TextColumn("[progress.description]{task.description}"),
121
- BarColumn(),
122
- TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
123
- console=console,
124
- ) as progress:
125
- read_task = progress.add_task("Reading chat data...", total=None)
126
-
127
- # Read chat data - handle array of arrays format
128
- data = json.loads(json_path.read_text())
129
- conversations = [chat for chat in data]
130
- progress.update(read_task, total=len(conversations))
131
-
132
- # Process each conversation
133
- messages_imported = 0
134
- chats_imported = 0
135
-
136
- for chat in conversations:
137
- # Convert to entity
138
- entity = format_chat_content(
139
- base_path=base_path,
140
- name=chat["name"],
141
- messages=chat["chat_messages"],
142
- created_at=chat["created_at"],
143
- modified_at=chat["updated_at"],
144
- )
145
-
146
- # Write file
147
- file_path = Path(f"{entity.frontmatter.metadata['permalink']}.md")
148
- await markdown_processor.write_file(file_path, entity)
149
-
150
- chats_imported += 1
151
- messages_imported += len(chat["chat_messages"])
152
- progress.update(read_task, advance=1)
153
-
154
- return {"conversations": chats_imported, "messages": messages_imported}
155
-
156
-
157
- async def get_markdown_processor() -> MarkdownProcessor:
158
- """Get MarkdownProcessor instance."""
21
+ async def get_importer_dependencies() -> Tuple[MarkdownProcessor, FileService]:
22
+ """Get MarkdownProcessor and FileService instances for importers."""
23
+ config = get_project_config()
24
+ app_config = ConfigManager().config
159
25
  entity_parser = EntityParser(config.home)
160
- return MarkdownProcessor(entity_parser)
26
+ markdown_processor = MarkdownProcessor(entity_parser, app_config=app_config)
27
+ file_service = FileService(config.home, markdown_processor, app_config=app_config)
28
+ return markdown_processor, file_service
161
29
 
162
30
 
163
31
  @claude_app.command(name="conversations", help="Import chat conversations from Claude.ai.")
@@ -179,35 +47,44 @@ def import_claude(
179
47
  After importing, run 'basic-memory sync' to index the new files.
180
48
  """
181
49
 
182
- with logfire.span("import claude conversations"): # pyright: ignore [reportGeneralTypeIssues]
183
- try:
184
- if not conversations_json.exists():
185
- typer.echo(f"Error: File not found: {conversations_json}", err=True)
186
- raise typer.Exit(1)
50
+ config = get_project_config()
51
+ try:
52
+ if not conversations_json.exists():
53
+ typer.echo(f"Error: File not found: {conversations_json}", err=True)
54
+ raise typer.Exit(1)
187
55
 
188
- # Get markdown processor
189
- markdown_processor = asyncio.run(get_markdown_processor())
56
+ # Get importer dependencies
57
+ markdown_processor, file_service = asyncio.run(get_importer_dependencies())
190
58
 
191
- # Process the file
192
- base_path = config.home / folder
193
- console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
194
- results = asyncio.run(
195
- process_conversations_json(conversations_json, base_path, markdown_processor)
196
- )
59
+ # Create the importer
60
+ importer = ClaudeConversationsImporter(config.home, markdown_processor, file_service)
197
61
 
198
- # Show results
199
- console.print(
200
- Panel(
201
- f"[green]Import complete![/green]\n\n"
202
- f"Imported {results['conversations']} conversations\n"
203
- f"Containing {results['messages']} messages",
204
- expand=False,
205
- )
206
- )
62
+ # Process the file
63
+ base_path = config.home / folder
64
+ console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
207
65
 
208
- console.print("\nRun 'basic-memory sync' to index the new files.")
66
+ # Run the import
67
+ with conversations_json.open("r", encoding="utf-8") as file:
68
+ json_data = json.load(file)
69
+ result = asyncio.run(importer.import_data(json_data, folder))
209
70
 
210
- except Exception as e:
211
- logger.error("Import failed")
212
- typer.echo(f"Error during import: {e}", err=True)
71
+ if not result.success: # pragma: no cover
72
+ typer.echo(f"Error during import: {result.error_message}", err=True)
213
73
  raise typer.Exit(1)
74
+
75
+ # Show results
76
+ console.print(
77
+ Panel(
78
+ f"[green]Import complete![/green]\n\n"
79
+ f"Imported {result.conversations} conversations\n"
80
+ f"Containing {result.messages} messages",
81
+ expand=False,
82
+ )
83
+ )
84
+
85
+ console.print("\nRun 'basic-memory sync' to index the new files.")
86
+
87
+ except Exception as e:
88
+ logger.error("Import failed")
89
+ typer.echo(f"Error during import: {e}", err=True)
90
+ raise typer.Exit(1)