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
@@ -2,27 +2,43 @@
2
2
 
3
3
  import asyncio
4
4
 
5
- import logfire
6
5
  import typer
7
6
  from loguru import logger
8
7
 
9
- from basic_memory.alembic import migrations
8
+ from basic_memory import db
10
9
  from basic_memory.cli.app import app
10
+ from basic_memory.config import ConfigManager, BasicMemoryConfig, save_basic_memory_config
11
11
 
12
12
 
13
13
  @app.command()
14
14
  def reset(
15
- reindex: bool = typer.Option(False, "--reindex", help="Rebuild indices from filesystem"),
15
+ reindex: bool = typer.Option(False, "--reindex", help="Rebuild db index from filesystem"),
16
16
  ): # pragma: no cover
17
17
  """Reset database (drop all tables and recreate)."""
18
- with logfire.span("reset"): # pyright: ignore [reportGeneralTypeIssues]
19
- if typer.confirm("This will delete all data in your db. Are you sure?"):
20
- logger.info("Resetting database...")
21
- asyncio.run(migrations.reset_database())
18
+ if typer.confirm("This will delete all data in your db. Are you sure?"):
19
+ logger.info("Resetting database...")
20
+ config_manager = ConfigManager()
21
+ app_config = config_manager.config
22
+ # Get database path
23
+ db_path = app_config.app_database_path
22
24
 
23
- if reindex:
24
- # Import and run sync
25
- from basic_memory.cli.commands.sync import sync
25
+ # Delete the database file if it exists
26
+ if db_path.exists():
27
+ db_path.unlink()
28
+ logger.info(f"Database file deleted: {db_path}")
26
29
 
27
- logger.info("Rebuilding search index from filesystem...")
28
- sync(watch=False) # pyright: ignore
30
+ # Reset project configuration
31
+ config = BasicMemoryConfig()
32
+ save_basic_memory_config(config_manager.config_file, config)
33
+ logger.info("Project configuration reset to default")
34
+
35
+ # Create a new empty database
36
+ asyncio.run(db.run_migrations(app_config))
37
+ logger.info("Database reset complete")
38
+
39
+ if reindex:
40
+ # Run database sync directly
41
+ from basic_memory.cli.commands.command_utils import run_sync
42
+
43
+ logger.info("Rebuilding search index from filesystem...")
44
+ asyncio.run(run_sync(project=None))
@@ -2,207 +2,24 @@
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
8
7
 
9
- import logfire
10
8
  import typer
9
+ from basic_memory.cli.app import import_app
10
+ from basic_memory.config import get_project_config
11
+ from basic_memory.importers import ChatGPTImporter
12
+ from basic_memory.markdown import EntityParser, MarkdownProcessor
11
13
  from loguru import logger
12
14
  from rich.console import Console
13
15
  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
16
 
21
17
  console = Console()
22
18
 
23
19
 
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
20
  async def get_markdown_processor() -> MarkdownProcessor:
205
21
  """Get MarkdownProcessor instance."""
22
+ config = get_project_config()
206
23
  entity_parser = EntityParser(config.home)
207
24
  return MarkdownProcessor(entity_parser)
208
25
 
@@ -226,38 +43,41 @@ def import_chatgpt(
226
43
  After importing, run 'basic-memory sync' to index the new files.
227
44
  """
228
45
 
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())
46
+ try:
47
+ if not conversations_json.exists(): # pragma: no cover
48
+ typer.echo(f"Error: File not found: {conversations_json}", err=True)
49
+ raise typer.Exit(1)
238
50
 
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
- )
51
+ # Get markdown processor
52
+ markdown_processor = asyncio.run(get_markdown_processor())
53
+ config = get_project_config()
54
+ # Process the file
55
+ base_path = config.home / folder
56
+ console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
57
+
58
+ # Create importer and run import
59
+ importer = ChatGPTImporter(config.home, markdown_processor)
60
+ with conversations_json.open("r", encoding="utf-8") as file:
61
+ json_data = json.load(file)
62
+ result = asyncio.run(importer.import_data(json_data, folder))
63
+
64
+ if not result.success: # pragma: no cover
65
+ typer.echo(f"Error during import: {result.error_message}", err=True)
66
+ raise typer.Exit(1)
247
67
 
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
- )
68
+ # Show results
69
+ console.print(
70
+ Panel(
71
+ f"[green]Import complete![/green]\n\n"
72
+ f"Imported {result.conversations} conversations\n"
73
+ f"Containing {result.messages} messages",
74
+ expand=False,
75
+ )
76
+ )
257
77
 
258
- console.print("\nRun 'basic-memory sync' to index the new files.")
78
+ console.print("\nRun 'basic-memory sync' to index the new files.")
259
79
 
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)
80
+ except Exception as e:
81
+ logger.error("Import failed")
82
+ typer.echo(f"Error during import: {e}", err=True)
83
+ raise typer.Exit(1)
@@ -2,160 +2,24 @@
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
8
7
 
9
- import logfire
10
8
  import typer
9
+ from basic_memory.cli.app import claude_app
10
+ from basic_memory.config import get_project_config
11
+ from basic_memory.importers.claude_conversations_importer import ClaudeConversationsImporter
12
+ from basic_memory.markdown import EntityParser, MarkdownProcessor
11
13
  from loguru import logger
12
14
  from rich.console import Console
13
15
  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
16
 
21
17
  console = Console()
22
18
 
23
19
 
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
20
  async def get_markdown_processor() -> MarkdownProcessor:
158
21
  """Get MarkdownProcessor instance."""
22
+ config = get_project_config()
159
23
  entity_parser = EntityParser(config.home)
160
24
  return MarkdownProcessor(entity_parser)
161
25
 
@@ -179,35 +43,44 @@ def import_claude(
179
43
  After importing, run 'basic-memory sync' to index the new files.
180
44
  """
181
45
 
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)
46
+ config = get_project_config()
47
+ try:
48
+ if not conversations_json.exists():
49
+ typer.echo(f"Error: File not found: {conversations_json}", err=True)
50
+ raise typer.Exit(1)
187
51
 
188
- # Get markdown processor
189
- markdown_processor = asyncio.run(get_markdown_processor())
52
+ # Get markdown processor
53
+ markdown_processor = asyncio.run(get_markdown_processor())
190
54
 
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
- )
55
+ # Create the importer
56
+ importer = ClaudeConversationsImporter(config.home, markdown_processor)
197
57
 
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
- )
58
+ # Process the file
59
+ base_path = config.home / folder
60
+ console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
207
61
 
208
- console.print("\nRun 'basic-memory sync' to index the new files.")
62
+ # Run the import
63
+ with conversations_json.open("r", encoding="utf-8") as file:
64
+ json_data = json.load(file)
65
+ result = asyncio.run(importer.import_data(json_data, folder))
209
66
 
210
- except Exception as e:
211
- logger.error("Import failed")
212
- typer.echo(f"Error during import: {e}", err=True)
67
+ if not result.success: # pragma: no cover
68
+ typer.echo(f"Error during import: {result.error_message}", err=True)
213
69
  raise typer.Exit(1)
70
+
71
+ # Show results
72
+ console.print(
73
+ Panel(
74
+ f"[green]Import complete![/green]\n\n"
75
+ f"Imported {result.conversations} conversations\n"
76
+ f"Containing {result.messages} messages",
77
+ expand=False,
78
+ )
79
+ )
80
+
81
+ console.print("\nRun 'basic-memory sync' to index the new files.")
82
+
83
+ except Exception as e:
84
+ logger.error("Import failed")
85
+ typer.echo(f"Error during import: {e}", err=True)
86
+ raise typer.Exit(1)