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.
- basic_memory/__init__.py +7 -1
- basic_memory/alembic/env.py +1 -1
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -5
- basic_memory/api/app.py +43 -13
- basic_memory/api/routers/__init__.py +4 -2
- basic_memory/api/routers/directory_router.py +63 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +127 -38
- basic_memory/api/routers/management_router.py +78 -0
- basic_memory/api/routers/memory_router.py +4 -59
- basic_memory/api/routers/project_router.py +230 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/search_router.py +3 -21
- basic_memory/api/routers/utils.py +130 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/cli/app.py +20 -21
- basic_memory/cli/commands/__init__.py +2 -1
- basic_memory/cli/commands/auth.py +136 -0
- basic_memory/cli/commands/db.py +3 -3
- basic_memory/cli/commands/import_chatgpt.py +31 -207
- basic_memory/cli/commands/import_claude_conversations.py +16 -142
- basic_memory/cli/commands/import_claude_projects.py +33 -143
- basic_memory/cli/commands/import_memory_json.py +26 -83
- basic_memory/cli/commands/mcp.py +71 -18
- basic_memory/cli/commands/project.py +99 -67
- basic_memory/cli/commands/status.py +19 -9
- basic_memory/cli/commands/sync.py +44 -58
- basic_memory/cli/main.py +1 -5
- basic_memory/config.py +144 -88
- basic_memory/db.py +6 -4
- basic_memory/deps.py +227 -30
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +222 -0
- basic_memory/importers/claude_conversations_importer.py +172 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +93 -0
- basic_memory/importers/utils.py +58 -0
- basic_memory/markdown/entity_parser.py +5 -2
- basic_memory/mcp/auth_provider.py +270 -0
- basic_memory/mcp/external_auth_provider.py +321 -0
- basic_memory/mcp/project_session.py +103 -0
- basic_memory/mcp/prompts/continue_conversation.py +18 -68
- basic_memory/mcp/prompts/recent_activity.py +19 -3
- basic_memory/mcp/prompts/search.py +14 -140
- basic_memory/mcp/prompts/utils.py +3 -3
- basic_memory/mcp/{tools → resources}/project_info.py +6 -2
- basic_memory/mcp/server.py +82 -8
- basic_memory/mcp/supabase_auth_provider.py +463 -0
- basic_memory/mcp/tools/__init__.py +20 -0
- basic_memory/mcp/tools/build_context.py +11 -1
- basic_memory/mcp/tools/canvas.py +15 -2
- basic_memory/mcp/tools/delete_note.py +12 -4
- basic_memory/mcp/tools/edit_note.py +297 -0
- basic_memory/mcp/tools/list_directory.py +154 -0
- basic_memory/mcp/tools/move_note.py +87 -0
- basic_memory/mcp/tools/project_management.py +300 -0
- basic_memory/mcp/tools/read_content.py +15 -6
- basic_memory/mcp/tools/read_note.py +17 -5
- basic_memory/mcp/tools/recent_activity.py +11 -2
- basic_memory/mcp/tools/search.py +10 -1
- basic_memory/mcp/tools/utils.py +137 -12
- basic_memory/mcp/tools/write_note.py +11 -15
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +16 -4
- basic_memory/models/project.py +80 -0
- basic_memory/models/search.py +8 -5
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +8 -3
- basic_memory/repository/observation_repository.py +35 -3
- basic_memory/repository/project_info_repository.py +3 -2
- basic_memory/repository/project_repository.py +85 -0
- basic_memory/repository/relation_repository.py +8 -2
- basic_memory/repository/repository.py +107 -15
- basic_memory/repository/search_repository.py +87 -27
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +34 -0
- basic_memory/schemas/memory.py +26 -12
- basic_memory/schemas/project_info.py +112 -2
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +56 -2
- basic_memory/schemas/search.py +1 -1
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +208 -95
- basic_memory/services/directory_service.py +167 -0
- basic_memory/services/entity_service.py +385 -5
- basic_memory/services/exceptions.py +6 -0
- basic_memory/services/file_service.py +14 -15
- basic_memory/services/initialization.py +144 -67
- basic_memory/services/link_resolver.py +16 -8
- basic_memory/services/project_service.py +548 -0
- basic_memory/services/search_service.py +77 -2
- basic_memory/sync/background_sync.py +25 -0
- basic_memory/sync/sync_service.py +10 -9
- basic_memory/sync/watch_service.py +63 -39
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/METADATA +23 -1
- basic_memory-0.13.0b2.dist-info/RECORD +132 -0
- basic_memory/api/routers/project_info_router.py +0 -274
- basic_memory/mcp/main.py +0 -24
- basic_memory-0.12.3.dist-info/RECORD +0 -100
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/WHEEL +0 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
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
|
-
|
|
192
|
-
|
|
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 {
|
|
200
|
-
f"Containing {
|
|
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
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|