basic-memory 0.12.3__py3-none-any.whl → 0.13.0b2__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 (107) hide show
  1. basic_memory/__init__.py +7 -1
  2. basic_memory/alembic/env.py +1 -1
  3. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  4. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -5
  5. basic_memory/api/app.py +43 -13
  6. basic_memory/api/routers/__init__.py +4 -2
  7. basic_memory/api/routers/directory_router.py +63 -0
  8. basic_memory/api/routers/importer_router.py +152 -0
  9. basic_memory/api/routers/knowledge_router.py +127 -38
  10. basic_memory/api/routers/management_router.py +78 -0
  11. basic_memory/api/routers/memory_router.py +4 -59
  12. basic_memory/api/routers/project_router.py +230 -0
  13. basic_memory/api/routers/prompt_router.py +260 -0
  14. basic_memory/api/routers/search_router.py +3 -21
  15. basic_memory/api/routers/utils.py +130 -0
  16. basic_memory/api/template_loader.py +292 -0
  17. basic_memory/cli/app.py +20 -21
  18. basic_memory/cli/commands/__init__.py +2 -1
  19. basic_memory/cli/commands/auth.py +136 -0
  20. basic_memory/cli/commands/db.py +3 -3
  21. basic_memory/cli/commands/import_chatgpt.py +31 -207
  22. basic_memory/cli/commands/import_claude_conversations.py +16 -142
  23. basic_memory/cli/commands/import_claude_projects.py +33 -143
  24. basic_memory/cli/commands/import_memory_json.py +26 -83
  25. basic_memory/cli/commands/mcp.py +71 -18
  26. basic_memory/cli/commands/project.py +99 -67
  27. basic_memory/cli/commands/status.py +19 -9
  28. basic_memory/cli/commands/sync.py +44 -58
  29. basic_memory/cli/main.py +1 -5
  30. basic_memory/config.py +144 -88
  31. basic_memory/db.py +6 -4
  32. basic_memory/deps.py +227 -30
  33. basic_memory/importers/__init__.py +27 -0
  34. basic_memory/importers/base.py +79 -0
  35. basic_memory/importers/chatgpt_importer.py +222 -0
  36. basic_memory/importers/claude_conversations_importer.py +172 -0
  37. basic_memory/importers/claude_projects_importer.py +148 -0
  38. basic_memory/importers/memory_json_importer.py +93 -0
  39. basic_memory/importers/utils.py +58 -0
  40. basic_memory/markdown/entity_parser.py +5 -2
  41. basic_memory/mcp/auth_provider.py +270 -0
  42. basic_memory/mcp/external_auth_provider.py +321 -0
  43. basic_memory/mcp/project_session.py +103 -0
  44. basic_memory/mcp/prompts/continue_conversation.py +18 -68
  45. basic_memory/mcp/prompts/recent_activity.py +19 -3
  46. basic_memory/mcp/prompts/search.py +14 -140
  47. basic_memory/mcp/prompts/utils.py +3 -3
  48. basic_memory/mcp/{tools → resources}/project_info.py +6 -2
  49. basic_memory/mcp/server.py +82 -8
  50. basic_memory/mcp/supabase_auth_provider.py +463 -0
  51. basic_memory/mcp/tools/__init__.py +20 -0
  52. basic_memory/mcp/tools/build_context.py +11 -1
  53. basic_memory/mcp/tools/canvas.py +15 -2
  54. basic_memory/mcp/tools/delete_note.py +12 -4
  55. basic_memory/mcp/tools/edit_note.py +297 -0
  56. basic_memory/mcp/tools/list_directory.py +154 -0
  57. basic_memory/mcp/tools/move_note.py +87 -0
  58. basic_memory/mcp/tools/project_management.py +300 -0
  59. basic_memory/mcp/tools/read_content.py +15 -6
  60. basic_memory/mcp/tools/read_note.py +17 -5
  61. basic_memory/mcp/tools/recent_activity.py +11 -2
  62. basic_memory/mcp/tools/search.py +10 -1
  63. basic_memory/mcp/tools/utils.py +137 -12
  64. basic_memory/mcp/tools/write_note.py +11 -15
  65. basic_memory/models/__init__.py +3 -2
  66. basic_memory/models/knowledge.py +16 -4
  67. basic_memory/models/project.py +80 -0
  68. basic_memory/models/search.py +8 -5
  69. basic_memory/repository/__init__.py +2 -0
  70. basic_memory/repository/entity_repository.py +8 -3
  71. basic_memory/repository/observation_repository.py +35 -3
  72. basic_memory/repository/project_info_repository.py +3 -2
  73. basic_memory/repository/project_repository.py +85 -0
  74. basic_memory/repository/relation_repository.py +8 -2
  75. basic_memory/repository/repository.py +107 -15
  76. basic_memory/repository/search_repository.py +87 -27
  77. basic_memory/schemas/__init__.py +6 -0
  78. basic_memory/schemas/directory.py +30 -0
  79. basic_memory/schemas/importer.py +34 -0
  80. basic_memory/schemas/memory.py +26 -12
  81. basic_memory/schemas/project_info.py +112 -2
  82. basic_memory/schemas/prompt.py +90 -0
  83. basic_memory/schemas/request.py +56 -2
  84. basic_memory/schemas/search.py +1 -1
  85. basic_memory/services/__init__.py +2 -1
  86. basic_memory/services/context_service.py +208 -95
  87. basic_memory/services/directory_service.py +167 -0
  88. basic_memory/services/entity_service.py +385 -5
  89. basic_memory/services/exceptions.py +6 -0
  90. basic_memory/services/file_service.py +14 -15
  91. basic_memory/services/initialization.py +144 -67
  92. basic_memory/services/link_resolver.py +16 -8
  93. basic_memory/services/project_service.py +548 -0
  94. basic_memory/services/search_service.py +77 -2
  95. basic_memory/sync/background_sync.py +25 -0
  96. basic_memory/sync/sync_service.py +10 -9
  97. basic_memory/sync/watch_service.py +63 -39
  98. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  99. basic_memory/templates/prompts/search.hbs +101 -0
  100. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/METADATA +23 -1
  101. basic_memory-0.13.0b2.dist-info/RECORD +132 -0
  102. basic_memory/api/routers/project_info_router.py +0 -274
  103. basic_memory/mcp/main.py +0 -24
  104. basic_memory-0.12.3.dist-info/RECORD +0 -100
  105. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/WHEEL +0 -0
  106. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/entry_points.txt +0 -0
  107. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/licenses/LICENSE +0 -0
@@ -2,203 +2,21 @@
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
8
  import typer
10
9
  from basic_memory.cli.app import import_app
11
10
  from basic_memory.config import config
11
+ from basic_memory.importers import ChatGPTImporter
12
12
  from basic_memory.markdown import EntityParser, MarkdownProcessor
13
- from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
14
13
  from loguru import logger
15
14
  from rich.console import Console
16
15
  from rich.panel import Panel
17
- from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
18
16
 
19
17
  console = Console()
20
18
 
21
19
 
22
- def clean_filename(text: str) -> str:
23
- """Convert text to safe filename."""
24
- clean = "".join(c if c.isalnum() else "-" for c in text.lower()).strip("-")
25
- return clean
26
-
27
-
28
- def format_timestamp(ts: float) -> str:
29
- """Format Unix timestamp for display."""
30
- dt = datetime.fromtimestamp(ts)
31
- return dt.strftime("%Y-%m-%d %H:%M:%S")
32
-
33
-
34
- def get_message_content(message: Dict[str, Any]) -> str:
35
- """Extract clean message content."""
36
- if not message or "content" not in message:
37
- return "" # pragma: no cover
38
-
39
- content = message["content"]
40
- if content.get("content_type") == "text":
41
- return "\n".join(content.get("parts", []))
42
- elif content.get("content_type") == "code":
43
- return f"```{content.get('language', '')}\n{content.get('text', '')}\n```"
44
- return "" # pragma: no cover
45
-
46
-
47
- def traverse_messages(
48
- mapping: Dict[str, Any], root_id: Optional[str], seen: Set[str]
49
- ) -> List[Dict[str, Any]]:
50
- """Traverse message tree and return messages in order."""
51
- messages = []
52
- node = mapping.get(root_id) if root_id else None
53
-
54
- while node:
55
- if node["id"] not in seen and node.get("message"):
56
- seen.add(node["id"])
57
- messages.append(node["message"])
58
-
59
- # Follow children
60
- children = node.get("children", [])
61
- for child_id in children:
62
- child_msgs = traverse_messages(mapping, child_id, seen)
63
- messages.extend(child_msgs)
64
-
65
- break # Don't follow siblings
66
-
67
- return messages
68
-
69
-
70
- def format_chat_markdown(
71
- title: str,
72
- mapping: Dict[str, Any],
73
- root_id: Optional[str],
74
- created_at: float,
75
- modified_at: float,
76
- ) -> str:
77
- """Format chat as clean markdown."""
78
-
79
- # Start with title
80
- lines = [f"# {title}\n"]
81
-
82
- # Traverse message tree
83
- seen_msgs = set()
84
- messages = traverse_messages(mapping, root_id, seen_msgs)
85
-
86
- # Format each message
87
- for msg in messages:
88
- # Skip hidden messages
89
- if msg.get("metadata", {}).get("is_visually_hidden_from_conversation"):
90
- continue
91
-
92
- # Get author and timestamp
93
- author = msg["author"]["role"].title()
94
- ts = format_timestamp(msg["create_time"]) if msg.get("create_time") else ""
95
-
96
- # Add message header
97
- lines.append(f"### {author} ({ts})")
98
-
99
- # Add message content
100
- content = get_message_content(msg)
101
- if content:
102
- lines.append(content)
103
-
104
- # Add spacing
105
- lines.append("")
106
-
107
- return "\n".join(lines)
108
-
109
-
110
- def format_chat_content(folder: str, conversation: Dict[str, Any]) -> EntityMarkdown:
111
- """Convert chat conversation to Basic Memory entity."""
112
-
113
- # Extract timestamps
114
- created_at = conversation["create_time"]
115
- modified_at = conversation["update_time"]
116
-
117
- root_id = None
118
- # Find root message
119
- for node_id, node in conversation["mapping"].items():
120
- if node.get("parent") is None:
121
- root_id = node_id
122
- break
123
-
124
- # Generate permalink
125
- date_prefix = datetime.fromtimestamp(created_at).strftime("%Y%m%d")
126
- clean_title = clean_filename(conversation["title"])
127
-
128
- # Format content
129
- content = format_chat_markdown(
130
- title=conversation["title"],
131
- mapping=conversation["mapping"],
132
- root_id=root_id,
133
- created_at=created_at,
134
- modified_at=modified_at,
135
- )
136
-
137
- # Create entity
138
- entity = EntityMarkdown(
139
- frontmatter=EntityFrontmatter(
140
- metadata={
141
- "type": "conversation",
142
- "title": conversation["title"],
143
- "created": format_timestamp(created_at),
144
- "modified": format_timestamp(modified_at),
145
- "permalink": f"{folder}/{date_prefix}-{clean_title}",
146
- }
147
- ),
148
- content=content,
149
- )
150
-
151
- return entity
152
-
153
-
154
- async def process_chatgpt_json(
155
- json_path: Path, folder: str, markdown_processor: MarkdownProcessor
156
- ) -> Dict[str, int]:
157
- """Import conversations from ChatGPT JSON format."""
158
-
159
- with Progress(
160
- SpinnerColumn(),
161
- TextColumn("[progress.description]{task.description}"),
162
- BarColumn(),
163
- TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
164
- console=console,
165
- ) as progress:
166
- read_task = progress.add_task("Reading chat data...", total=None)
167
-
168
- # Read conversations
169
- conversations = json.loads(json_path.read_text(encoding="utf-8"))
170
- progress.update(read_task, total=len(conversations))
171
-
172
- # Process each conversation
173
- messages_imported = 0
174
- chats_imported = 0
175
-
176
- for chat in conversations:
177
- # Convert to entity
178
- entity = format_chat_content(folder, chat)
179
-
180
- # Write file
181
- file_path = config.home / f"{entity.frontmatter.metadata['permalink']}.md"
182
- # logger.info(f"Writing file: {file_path.absolute()}")
183
- await markdown_processor.write_file(file_path, entity)
184
-
185
- # Count messages
186
- msg_count = sum(
187
- 1
188
- for node in chat["mapping"].values()
189
- if node.get("message")
190
- and not node.get("message", {})
191
- .get("metadata", {})
192
- .get("is_visually_hidden_from_conversation")
193
- )
194
-
195
- chats_imported += 1
196
- messages_imported += msg_count
197
- progress.update(read_task, advance=1)
198
-
199
- return {"conversations": chats_imported, "messages": messages_imported}
200
-
201
-
202
20
  async def get_markdown_processor() -> MarkdownProcessor:
203
21
  """Get MarkdownProcessor instance."""
204
22
  entity_parser = EntityParser(config.home)
@@ -225,30 +43,36 @@ def import_chatgpt(
225
43
  """
226
44
 
227
45
  try:
228
- if conversations_json:
229
- if not conversations_json.exists():
230
- typer.echo(f"Error: File not found: {conversations_json}", err=True)
231
- raise typer.Exit(1)
232
-
233
- # Get markdown processor
234
- markdown_processor = asyncio.run(get_markdown_processor())
235
-
236
- # Process the file
237
- base_path = config.home / folder
238
- console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
239
- results = asyncio.run(
240
- process_chatgpt_json(conversations_json, folder, markdown_processor)
241
- )
242
-
243
- # Show results
244
- console.print(
245
- Panel(
246
- f"[green]Import complete![/green]\n\n"
247
- f"Imported {results['conversations']} conversations\n"
248
- f"Containing {results['messages']} messages",
249
- expand=False,
250
- )
46
+ if not conversations_json.exists(): # pragma: no cover
47
+ typer.echo(f"Error: File not found: {conversations_json}", err=True)
48
+ raise typer.Exit(1)
49
+
50
+ # Get markdown processor
51
+ markdown_processor = asyncio.run(get_markdown_processor())
52
+
53
+ # Process the file
54
+ base_path = config.home / folder
55
+ console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
56
+
57
+ # Create importer and run import
58
+ importer = ChatGPTImporter(config.home, markdown_processor)
59
+ with conversations_json.open("r", encoding="utf-8") as file:
60
+ json_data = json.load(file)
61
+ result = asyncio.run(importer.import_data(json_data, folder))
62
+
63
+ if not result.success: # pragma: no cover
64
+ typer.echo(f"Error during import: {result.error_message}", err=True)
65
+ raise typer.Exit(1)
66
+
67
+ # Show results
68
+ console.print(
69
+ Panel(
70
+ f"[green]Import complete![/green]\n\n"
71
+ f"Imported {result.conversations} conversations\n"
72
+ f"Containing {result.messages} messages",
73
+ expand=False,
251
74
  )
75
+ )
252
76
 
253
77
  console.print("\nRun 'basic-memory sync' to index the new files.")
254
78
 
@@ -2,156 +2,21 @@
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
8
  import typer
10
9
  from basic_memory.cli.app import claude_app
11
10
  from basic_memory.config import config
11
+ from basic_memory.importers.claude_conversations_importer import ClaudeConversationsImporter
12
12
  from basic_memory.markdown import EntityParser, MarkdownProcessor
13
- from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
14
13
  from loguru import logger
15
14
  from rich.console import Console
16
15
  from rich.panel import Panel
17
- from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
18
16
 
19
17
  console = Console()
20
18
 
21
19
 
22
- def clean_filename(text: str) -> str:
23
- """Convert text to safe filename."""
24
- # Remove invalid characters and convert spaces
25
- clean = "".join(c if c.isalnum() else "-" for c in text.lower()).strip("-")
26
- return clean
27
-
28
-
29
- def format_timestamp(ts: str) -> str:
30
- """Format ISO timestamp for display."""
31
- dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
32
- return dt.strftime("%Y-%m-%d %H:%M:%S")
33
-
34
-
35
- def format_chat_markdown(
36
- name: str, messages: List[Dict[str, Any]], created_at: str, modified_at: str, permalink: str
37
- ) -> str:
38
- """Format chat as clean markdown."""
39
-
40
- # Start with frontmatter and title
41
- lines = [
42
- f"# {name}\n",
43
- ]
44
-
45
- # Add messages
46
- for msg in messages:
47
- # Format timestamp
48
- ts = format_timestamp(msg["created_at"])
49
-
50
- # Add message header
51
- lines.append(f"### {msg['sender'].title()} ({ts})")
52
-
53
- # Handle message content
54
- content = msg.get("text", "")
55
- if msg.get("content"):
56
- content = " ".join(c.get("text", "") for c in msg["content"])
57
- lines.append(content)
58
-
59
- # Handle attachments
60
- attachments = msg.get("attachments", [])
61
- for attachment in attachments:
62
- if "file_name" in attachment:
63
- lines.append(f"\n**Attachment: {attachment['file_name']}**")
64
- if "extracted_content" in attachment:
65
- lines.append("```")
66
- lines.append(attachment["extracted_content"])
67
- lines.append("```")
68
-
69
- # Add spacing between messages
70
- lines.append("")
71
-
72
- return "\n".join(lines)
73
-
74
-
75
- def format_chat_content(
76
- base_path: Path, name: str, messages: List[Dict[str, Any]], created_at: str, modified_at: str
77
- ) -> EntityMarkdown:
78
- """Convert chat messages to Basic Memory entity format."""
79
-
80
- # Generate permalink
81
- date_prefix = datetime.fromisoformat(created_at.replace("Z", "+00:00")).strftime("%Y%m%d")
82
- clean_title = clean_filename(name)
83
- permalink = f"{base_path}/{date_prefix}-{clean_title}"
84
-
85
- # Format content
86
- content = format_chat_markdown(
87
- name=name,
88
- messages=messages,
89
- created_at=created_at,
90
- modified_at=modified_at,
91
- permalink=permalink,
92
- )
93
-
94
- # Create entity
95
- entity = EntityMarkdown(
96
- frontmatter=EntityFrontmatter(
97
- metadata={
98
- "type": "conversation",
99
- "title": name,
100
- "created": created_at,
101
- "modified": modified_at,
102
- "permalink": permalink,
103
- }
104
- ),
105
- content=content,
106
- )
107
-
108
- return entity
109
-
110
-
111
- async def process_conversations_json(
112
- json_path: Path, base_path: Path, markdown_processor: MarkdownProcessor
113
- ) -> Dict[str, int]:
114
- """Import chat data from conversations2.json format."""
115
-
116
- with Progress(
117
- SpinnerColumn(),
118
- TextColumn("[progress.description]{task.description}"),
119
- BarColumn(),
120
- TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
121
- console=console,
122
- ) as progress:
123
- read_task = progress.add_task("Reading chat data...", total=None)
124
-
125
- # Read chat data - handle array of arrays format
126
- data = json.loads(json_path.read_text(encoding="utf-8"))
127
- conversations = [chat for chat in data]
128
- progress.update(read_task, total=len(conversations))
129
-
130
- # Process each conversation
131
- messages_imported = 0
132
- chats_imported = 0
133
-
134
- for chat in conversations:
135
- # Convert to entity
136
- entity = format_chat_content(
137
- base_path=base_path,
138
- name=chat["name"],
139
- messages=chat["chat_messages"],
140
- created_at=chat["created_at"],
141
- modified_at=chat["updated_at"],
142
- )
143
-
144
- # Write file
145
- file_path = Path(f"{entity.frontmatter.metadata['permalink']}.md")
146
- await markdown_processor.write_file(file_path, entity)
147
-
148
- chats_imported += 1
149
- messages_imported += len(chat["chat_messages"])
150
- progress.update(read_task, advance=1)
151
-
152
- return {"conversations": chats_imported, "messages": messages_imported}
153
-
154
-
155
20
  async def get_markdown_processor() -> MarkdownProcessor:
156
21
  """Get MarkdownProcessor instance."""
157
22
  entity_parser = EntityParser(config.home)
@@ -185,19 +50,28 @@ def import_claude(
185
50
  # Get markdown processor
186
51
  markdown_processor = asyncio.run(get_markdown_processor())
187
52
 
53
+ # Create the importer
54
+ importer = ClaudeConversationsImporter(config.home, markdown_processor)
55
+
188
56
  # Process the file
189
57
  base_path = config.home / folder
190
58
  console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
191
- results = asyncio.run(
192
- process_conversations_json(conversations_json, base_path, markdown_processor)
193
- )
59
+
60
+ # Run the import
61
+ with conversations_json.open("r", encoding="utf-8") as file:
62
+ json_data = json.load(file)
63
+ result = asyncio.run(importer.import_data(json_data, folder))
64
+
65
+ if not result.success: # pragma: no cover
66
+ typer.echo(f"Error during import: {result.error_message}", err=True)
67
+ raise typer.Exit(1)
194
68
 
195
69
  # Show results
196
70
  console.print(
197
71
  Panel(
198
72
  f"[green]Import complete![/green]\n\n"
199
- f"Imported {results['conversations']} conversations\n"
200
- f"Containing {results['messages']} messages",
73
+ f"Imported {result.conversations} conversations\n"
74
+ f"Containing {result.messages} messages",
201
75
  expand=False,
202
76
  )
203
77
  )
@@ -3,138 +3,20 @@
3
3
  import asyncio
4
4
  import json
5
5
  from pathlib import Path
6
- from typing import Dict, Any, Annotated, Optional
6
+ from typing import Annotated
7
7
 
8
8
  import typer
9
9
  from basic_memory.cli.app import claude_app
10
10
  from basic_memory.config import config
11
+ from basic_memory.importers.claude_projects_importer import ClaudeProjectsImporter
11
12
  from basic_memory.markdown import EntityParser, MarkdownProcessor
12
- from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
13
13
  from loguru import logger
14
14
  from rich.console import Console
15
15
  from rich.panel import Panel
16
- from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
17
16
 
18
17
  console = Console()
19
18
 
20
19
 
21
- def clean_filename(text: str) -> str:
22
- """Convert text to safe filename."""
23
- clean = "".join(c if c.isalnum() else "-" for c in text.lower()).strip("-")
24
- return clean
25
-
26
-
27
- def format_project_markdown(project: Dict[str, Any], doc: Dict[str, Any]) -> EntityMarkdown:
28
- """Format a project document as a Basic Memory entity."""
29
-
30
- # Extract timestamps
31
- created_at = doc.get("created_at") or project["created_at"]
32
- modified_at = project["updated_at"]
33
-
34
- # Generate clean names for organization
35
- project_dir = clean_filename(project["name"])
36
- doc_file = clean_filename(doc["filename"])
37
-
38
- # Create entity
39
- entity = EntityMarkdown(
40
- frontmatter=EntityFrontmatter(
41
- metadata={
42
- "type": "project_doc",
43
- "title": doc["filename"],
44
- "created": created_at,
45
- "modified": modified_at,
46
- "permalink": f"{project_dir}/docs/{doc_file}",
47
- "project_name": project["name"],
48
- "project_uuid": project["uuid"],
49
- "doc_uuid": doc["uuid"],
50
- }
51
- ),
52
- content=doc["content"],
53
- )
54
-
55
- return entity
56
-
57
-
58
- def format_prompt_markdown(project: Dict[str, Any]) -> Optional[EntityMarkdown]:
59
- """Format project prompt template as a Basic Memory entity."""
60
-
61
- if not project.get("prompt_template"):
62
- return None
63
-
64
- # Extract timestamps
65
- created_at = project["created_at"]
66
- modified_at = project["updated_at"]
67
-
68
- # Generate clean project directory name
69
- project_dir = clean_filename(project["name"])
70
-
71
- # Create entity
72
- entity = EntityMarkdown(
73
- frontmatter=EntityFrontmatter(
74
- metadata={
75
- "type": "prompt_template",
76
- "title": f"Prompt Template: {project['name']}",
77
- "created": created_at,
78
- "modified": modified_at,
79
- "permalink": f"{project_dir}/prompt-template",
80
- "project_name": project["name"],
81
- "project_uuid": project["uuid"],
82
- }
83
- ),
84
- content=f"# Prompt Template: {project['name']}\n\n{project['prompt_template']}",
85
- )
86
-
87
- return entity
88
-
89
-
90
- async def process_projects_json(
91
- json_path: Path, base_path: Path, markdown_processor: MarkdownProcessor
92
- ) -> Dict[str, int]:
93
- """Import project data from Claude.ai projects.json format."""
94
-
95
- with Progress(
96
- SpinnerColumn(),
97
- TextColumn("[progress.description]{task.description}"),
98
- BarColumn(),
99
- TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
100
- console=console,
101
- ) as progress:
102
- read_task = progress.add_task("Reading project data...", total=None)
103
-
104
- # Read project data
105
- data = json.loads(json_path.read_text(encoding="utf-8"))
106
- progress.update(read_task, total=len(data))
107
-
108
- # Track import counts
109
- docs_imported = 0
110
- prompts_imported = 0
111
-
112
- # Process each project
113
- for project in data:
114
- project_dir = clean_filename(project["name"])
115
-
116
- # Create project directories
117
- docs_dir = base_path / project_dir / "docs"
118
- docs_dir.mkdir(parents=True, exist_ok=True)
119
-
120
- # Import prompt template if it exists
121
- if prompt_entity := format_prompt_markdown(project):
122
- file_path = base_path / f"{prompt_entity.frontmatter.metadata['permalink']}.md"
123
- await markdown_processor.write_file(file_path, prompt_entity)
124
- prompts_imported += 1
125
-
126
- # Import project documents
127
- for doc in project.get("docs", []):
128
- entity = format_project_markdown(project, doc)
129
- file_path = base_path / f"{entity.frontmatter.metadata['permalink']}.md"
130
- await markdown_processor.write_file(file_path, entity)
131
- docs_imported += 1
132
-
133
- progress.update(read_task, advance=1)
134
-
135
- return {"documents": docs_imported, "prompts": prompts_imported}
136
-
137
-
138
20
  async def get_markdown_processor() -> MarkdownProcessor:
139
21
  """Get MarkdownProcessor instance."""
140
22
  entity_parser = EntityParser(config.home)
@@ -160,30 +42,38 @@ def import_projects(
160
42
  After importing, run 'basic-memory sync' to index the new files.
161
43
  """
162
44
  try:
163
- if projects_json:
164
- if not projects_json.exists():
165
- typer.echo(f"Error: File not found: {projects_json}", err=True)
166
- raise typer.Exit(1)
167
-
168
- # Get markdown processor
169
- markdown_processor = asyncio.run(get_markdown_processor())
170
-
171
- # Process the file
172
- base_path = config.home / base_folder if base_folder else config.home
173
- console.print(f"\nImporting projects from {projects_json}...writing to {base_path}")
174
- results = asyncio.run(
175
- process_projects_json(projects_json, base_path, markdown_processor)
176
- )
177
-
178
- # Show results
179
- console.print(
180
- Panel(
181
- f"[green]Import complete![/green]\n\n"
182
- f"Imported {results['documents']} project documents\n"
183
- f"Imported {results['prompts']} prompt templates",
184
- expand=False,
185
- )
45
+ if not projects_json.exists():
46
+ typer.echo(f"Error: File not found: {projects_json}", err=True)
47
+ raise typer.Exit(1)
48
+
49
+ # Get markdown processor
50
+ markdown_processor = asyncio.run(get_markdown_processor())
51
+
52
+ # Create the importer
53
+ importer = ClaudeProjectsImporter(config.home, markdown_processor)
54
+
55
+ # Process the file
56
+ base_path = config.home / base_folder if base_folder else config.home
57
+ console.print(f"\nImporting projects from {projects_json}...writing to {base_path}")
58
+
59
+ # Run the import
60
+ with projects_json.open("r", encoding="utf-8") as file:
61
+ json_data = json.load(file)
62
+ result = asyncio.run(importer.import_data(json_data, base_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)
67
+
68
+ # Show results
69
+ console.print(
70
+ Panel(
71
+ f"[green]Import complete![/green]\n\n"
72
+ f"Imported {result.documents} project documents\n"
73
+ f"Imported {result.prompts} prompt templates",
74
+ expand=False,
186
75
  )
76
+ )
187
77
 
188
78
  console.print("\nRun 'basic-memory sync' to index the new files.")
189
79