thread-archive 0.0.2__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.
- thread_archive/__init__.py +34 -0
- thread_archive/_api.py +2235 -0
- thread_archive/_config.py +126 -0
- thread_archive/_importers/__init__.py +81 -0
- thread_archive/_importers/_continuation.py +136 -0
- thread_archive/_importers/_cursor.py +103 -0
- thread_archive/_importers/_events.py +244 -0
- thread_archive/_importers/_line_stream.py +193 -0
- thread_archive/_importers/_read.py +82 -0
- thread_archive/_importers/_result.py +17 -0
- thread_archive/_importers/_sidecar.py +113 -0
- thread_archive/_importers/_state.py +240 -0
- thread_archive/_importers/_titles.py +179 -0
- thread_archive/_importers/antigravity.py +254 -0
- thread_archive/_importers/claude_code.py +293 -0
- thread_archive/_importers/claude_science.py +330 -0
- thread_archive/_importers/cloth.py +20 -0
- thread_archive/_importers/codex.py +324 -0
- thread_archive/_importers/cowork.py +107 -0
- thread_archive/_importers/cursor.py +432 -0
- thread_archive/_importers/exports.py +389 -0
- thread_archive/_importers/grok.py +600 -0
- thread_archive/_importers/opencode.py +538 -0
- thread_archive/_knowledge/__init__.py +67 -0
- thread_archive/_knowledge/_claims.py +116 -0
- thread_archive/_knowledge/_community.py +75 -0
- thread_archive/_knowledge/graph.py +208 -0
- thread_archive/_knowledge/materialize.py +212 -0
- thread_archive/_knowledge/write.py +438 -0
- thread_archive/_launchd.py +167 -0
- thread_archive/_mcp/__init__.py +1 -0
- thread_archive/_mcp/librarian.py +151 -0
- thread_archive/_mcp/server.py +307 -0
- thread_archive/_retrieval/__init__.py +280 -0
- thread_archive/_retrieval/_classify.py +80 -0
- thread_archive/_retrieval/_codex.py +157 -0
- thread_archive/_retrieval/_context.py +123 -0
- thread_archive/_retrieval/_extract.py +184 -0
- thread_archive/_retrieval/embed.py +186 -0
- thread_archive/_retrieval/format.py +149 -0
- thread_archive/_retrieval/fts.py +538 -0
- thread_archive/_retrieval/rank.py +228 -0
- thread_archive/_retrieval/read.py +908 -0
- thread_archive/_retrieval/rerank.py +143 -0
- thread_archive/_retrieval/vectors.py +611 -0
- thread_archive/_scripts/__init__.py +1 -0
- thread_archive/_scripts/backfill_recompute.py +328 -0
- thread_archive/_scripts/backfill_reconcile.py +296 -0
- thread_archive/_scripts/denamespace_dedup_keys.py +120 -0
- thread_archive/_scripts/recover_dropped_events.py +190 -0
- thread_archive/_scripts/repair_grok_tool_names.py +118 -0
- thread_archive/_scripts/repair_grok_tool_names_backup_20260704T155625Z.json +275 -0
- thread_archive/_scripts/repair_grok_tool_names_plan_20260704.json +435 -0
- thread_archive/_setup/__init__.py +12 -0
- thread_archive/_setup/clients.py +89 -0
- thread_archive/_setup/wizard.py +474 -0
- thread_archive/_store/__init__.py +45 -0
- thread_archive/_store/_base.py +173 -0
- thread_archive/_store/_defaults.py +57 -0
- thread_archive/_store/_types.py +38 -0
- thread_archive/_store/models.py +370 -0
- thread_archive/_store/schema.py +84 -0
- thread_archive/_thread_import/__init__.py +40 -0
- thread_archive/_thread_import/api.py +78 -0
- thread_archive/_thread_import/event_builder.py +810 -0
- thread_archive/_thread_import/exporters/__init__.py +9 -0
- thread_archive/_thread_import/exporters/_cursor_kv_mixin.py +166 -0
- thread_archive/_thread_import/exporters/_cursor_parse_mixin.py +149 -0
- thread_archive/_thread_import/exporters/cursor.py +582 -0
- thread_archive/_thread_import/exporters/cursor_parse.py +145 -0
- thread_archive/_thread_import/parsers/__init__.py +191 -0
- thread_archive/_thread_import/parsers/base.py +698 -0
- thread_archive/_thread_import/parsers/chatgpt.py +722 -0
- thread_archive/_thread_import/parsers/chatgpt_content.py +332 -0
- thread_archive/_thread_import/parsers/claude.py +443 -0
- thread_archive/_thread_import/parsers/claude_code.py +1035 -0
- thread_archive/_thread_import/parsers/claude_code_blocks.py +415 -0
- thread_archive/_thread_import/parsers/claude_code_ide.py +122 -0
- thread_archive/_thread_import/parsers/claude_code_sessions.py +90 -0
- thread_archive/_thread_import/parsers/config/__init__.py +26 -0
- thread_archive/_thread_import/parsers/config/base.py +220 -0
- thread_archive/_thread_import/parsers/cursor.py +538 -0
- thread_archive/_thread_import/parsers/cursor_blocks.py +365 -0
- thread_archive/_thread_import/parsers/pipeline/__init__.py +29 -0
- thread_archive/_thread_import/parsers/pipeline/base.py +251 -0
- thread_archive/_thread_import/parsers/pipeline/interfaces.py +191 -0
- thread_archive/_thread_import/parsers/transformers/__init__.py +22 -0
- thread_archive/_thread_import/parsers/transformers/active_path.py +87 -0
- thread_archive/_thread_import/parsers/transformers/coalescing.py +389 -0
- thread_archive/_thread_import/parsers/transformers/ide_context.py +158 -0
- thread_archive/_thread_import/parsers/transformers/thinking_merge.py +68 -0
- thread_archive/_thread_import/parsers/types/__init__.py +56 -0
- thread_archive/_thread_import/parsers/types/chatgpt.py +99 -0
- thread_archive/_thread_import/parsers/types/claude.py +120 -0
- thread_archive/_thread_import/parsers/types/claude_code.py +139 -0
- thread_archive/_thread_import/parsers/types/cursor.py +109 -0
- thread_archive/_thread_import/parsers/validators/__init__.py +83 -0
- thread_archive/_thread_import/parsers/validators/base.py +168 -0
- thread_archive/_thread_import/parsers/validators/content.py +80 -0
- thread_archive/_thread_import/parsers/validators/referential.py +57 -0
- thread_archive/_thread_import/parsers/validators/thinking.py +143 -0
- thread_archive/_thread_import/parsers/validators/types.py +87 -0
- thread_archive/_thread_import/schemas/__init__.py +231 -0
- thread_archive/_thread_import/schemas/chatgpt/v1.json +197 -0
- thread_archive/_thread_import/schemas/chatgpt/v2.json +203 -0
- thread_archive/_thread_import/schemas/claude/v1.json +188 -0
- thread_archive/_thread_import/schemas/claude_code/v1.json +183 -0
- thread_archive/_thread_import/schemas/cursor/v1.json +143 -0
- thread_archive/_thread_import/schemas/cursor/v2.json +200 -0
- thread_archive/_thread_import/timestamps.py +57 -0
- thread_archive/_thread_import/tool_names.py +48 -0
- thread_archive/_truth/__init__.py +40 -0
- thread_archive/_truth/jsonl_log.py +2340 -0
- thread_archive/_truth/repair.py +322 -0
- thread_archive/_watcher/__init__.py +57 -0
- thread_archive/_watcher/base.py +65 -0
- thread_archive/_watcher/daemon.py +289 -0
- thread_archive/_watcher/export_drop.py +203 -0
- thread_archive/_watcher/exthost.py +316 -0
- thread_archive/_watcher/lazy.py +117 -0
- thread_archive/_watcher/sources.py +616 -0
- thread_archive/_web/__init__.py +15 -0
- thread_archive/_web/server.py +299 -0
- thread_archive/_web/static/assets/index-DECSH1Mz.css +10 -0
- thread_archive/_web/static/assets/index-Djq0FK0y.js +101 -0
- thread_archive/_web/static/index.html +13 -0
- thread_archive/cli.py +746 -0
- thread_archive-0.0.2.dist-info/METADATA +296 -0
- thread_archive-0.0.2.dist-info/RECORD +132 -0
- thread_archive-0.0.2.dist-info/WHEEL +4 -0
- thread_archive-0.0.2.dist-info/entry_points.txt +6 -0
- thread_archive-0.0.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IDE context transformer for Claude Code.
|
|
3
|
+
|
|
4
|
+
Claude Code embeds IDE context (opened files, selections) as XML-like
|
|
5
|
+
tags in user messages. This transformer extracts them as structured
|
|
6
|
+
ide_context content blocks.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple, cast
|
|
11
|
+
|
|
12
|
+
from ..base import ContentBlock
|
|
13
|
+
from ..pipeline.interfaces import RawMessage
|
|
14
|
+
|
|
15
|
+
# Regex patterns for IDE context tags
|
|
16
|
+
_IDE_OPENED_FILE_PATTERN = re.compile(
|
|
17
|
+
r"<ide_opened_file>(.*?)</ide_opened_file>", re.DOTALL
|
|
18
|
+
)
|
|
19
|
+
_IDE_SELECTION_PATTERN = re.compile(
|
|
20
|
+
r"<ide_selection>(.*?)</ide_selection>", re.DOTALL
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class IDEContextTransformer:
|
|
25
|
+
"""Extracts IDE context from user messages.
|
|
26
|
+
|
|
27
|
+
Claude Code embeds IDE state in user messages as XML-like tags:
|
|
28
|
+
- <ide_opened_file>...</ide_opened_file>
|
|
29
|
+
- <ide_selection>...</ide_selection>
|
|
30
|
+
|
|
31
|
+
This transformer:
|
|
32
|
+
1. Finds IDE context tags in message content
|
|
33
|
+
2. Extracts them as ide_context content blocks
|
|
34
|
+
3. Updates the message text to remove the tags
|
|
35
|
+
|
|
36
|
+
Note: This operates on text content, so should run after
|
|
37
|
+
basic content block extraction.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def transform(self, messages: List[RawMessage]) -> List[RawMessage]:
|
|
41
|
+
"""Extract IDE context from messages.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
messages: Messages to transform
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Messages with IDE context extracted
|
|
48
|
+
"""
|
|
49
|
+
for msg in messages:
|
|
50
|
+
if msg.role != "user":
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
self._extract_ide_context(msg)
|
|
54
|
+
|
|
55
|
+
return messages
|
|
56
|
+
|
|
57
|
+
def _extract_ide_context(self, msg: RawMessage) -> None:
|
|
58
|
+
"""Extract IDE context from a user message.
|
|
59
|
+
|
|
60
|
+
Modifies msg.content_blocks in place.
|
|
61
|
+
"""
|
|
62
|
+
# Find text blocks to process
|
|
63
|
+
text_blocks = [
|
|
64
|
+
(i, b)
|
|
65
|
+
for i, b in enumerate(msg.content_blocks)
|
|
66
|
+
if b.get("type") == "text"
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
if not text_blocks:
|
|
70
|
+
# Try raw content if no blocks yet
|
|
71
|
+
if isinstance(msg.content, str):
|
|
72
|
+
cleaned, ide_blocks = self._extract_from_text(msg.content)
|
|
73
|
+
if ide_blocks:
|
|
74
|
+
# Update content
|
|
75
|
+
msg.content = cleaned
|
|
76
|
+
# Add ide_context blocks
|
|
77
|
+
base_seq = len(msg.content_blocks)
|
|
78
|
+
for i, block in enumerate(ide_blocks):
|
|
79
|
+
block["seq"] = base_seq + i
|
|
80
|
+
msg.content_blocks.append(cast(ContentBlock, block))
|
|
81
|
+
# Update existing text block if present
|
|
82
|
+
if msg.content_blocks and msg.content_blocks[0].get("type") == "text":
|
|
83
|
+
cast(Dict[str, Any], msg.content_blocks[0])["text"] = cleaned
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
# Process each text block
|
|
87
|
+
ide_blocks_to_add: List[Dict[str, Any]] = []
|
|
88
|
+
for idx, block in text_blocks:
|
|
89
|
+
text = block.get("text", "")
|
|
90
|
+
if not text:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
cleaned, extracted = self._extract_from_text(text)
|
|
94
|
+
|
|
95
|
+
# Update the text block
|
|
96
|
+
cast(Dict[str, Any], block)["text"] = cleaned
|
|
97
|
+
|
|
98
|
+
# Collect IDE blocks
|
|
99
|
+
ide_blocks_to_add.extend(extracted)
|
|
100
|
+
|
|
101
|
+
# Add IDE blocks at the end
|
|
102
|
+
if ide_blocks_to_add:
|
|
103
|
+
base_seq = len(msg.content_blocks)
|
|
104
|
+
for i, block in enumerate(ide_blocks_to_add):
|
|
105
|
+
block["seq"] = base_seq + i
|
|
106
|
+
msg.content_blocks.append(cast(ContentBlock, block))
|
|
107
|
+
|
|
108
|
+
def _extract_from_text(
|
|
109
|
+
self, text: str
|
|
110
|
+
) -> Tuple[str, List[Dict[str, Any]]]:
|
|
111
|
+
"""Extract IDE context tags from text.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Tuple of (cleaned_text, list of ide_context blocks)
|
|
115
|
+
"""
|
|
116
|
+
ide_blocks: List[Dict[str, Any]] = []
|
|
117
|
+
|
|
118
|
+
# Extract <ide_opened_file> tags
|
|
119
|
+
for match in _IDE_OPENED_FILE_PATTERN.finditer(text):
|
|
120
|
+
content = match.group(1).strip()
|
|
121
|
+
file_path = self._extract_file_path(content)
|
|
122
|
+
|
|
123
|
+
ide_blocks.append({
|
|
124
|
+
"type": "ide_context",
|
|
125
|
+
"context_type": "opened_file",
|
|
126
|
+
"file_path": file_path,
|
|
127
|
+
"raw_content": content,
|
|
128
|
+
"seq": 0, # Will be renumbered
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
# Extract <ide_selection> tags
|
|
132
|
+
for match in _IDE_SELECTION_PATTERN.finditer(text):
|
|
133
|
+
content = match.group(1).strip()
|
|
134
|
+
|
|
135
|
+
ide_blocks.append({
|
|
136
|
+
"type": "ide_context",
|
|
137
|
+
"context_type": "selection",
|
|
138
|
+
"raw_content": content,
|
|
139
|
+
"seq": 0, # Will be renumbered
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
# Remove tags from text
|
|
143
|
+
cleaned = _IDE_OPENED_FILE_PATTERN.sub("", text)
|
|
144
|
+
cleaned = _IDE_SELECTION_PATTERN.sub("", cleaned)
|
|
145
|
+
cleaned = cleaned.strip()
|
|
146
|
+
|
|
147
|
+
return cleaned, ide_blocks
|
|
148
|
+
|
|
149
|
+
def _extract_file_path(self, content: str) -> Optional[str]:
|
|
150
|
+
"""Extract file path from opened file content.
|
|
151
|
+
|
|
152
|
+
Content is typically: "The user opened the file {path} in the IDE..."
|
|
153
|
+
"""
|
|
154
|
+
if "opened the file " in content:
|
|
155
|
+
path_match = re.search(r"opened the file ([^\s]+)", content)
|
|
156
|
+
if path_match:
|
|
157
|
+
return path_match.group(1)
|
|
158
|
+
return None
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Thinking merge transformer for Claude Code.
|
|
3
|
+
|
|
4
|
+
In Claude Code exports, thinking-only messages (messages with only thinking
|
|
5
|
+
blocks and no text) should be merged into their parent assistant message's
|
|
6
|
+
content_blocks.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, List, Set
|
|
10
|
+
|
|
11
|
+
from ..pipeline.interfaces import RawMessage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ThinkingMergeTransformer:
|
|
15
|
+
"""Merges thinking-only messages into parent assistant messages.
|
|
16
|
+
|
|
17
|
+
In the normalized data model, thinking blocks should be nested within
|
|
18
|
+
the parent message's content_blocks, not as separate messages.
|
|
19
|
+
|
|
20
|
+
This transformer:
|
|
21
|
+
1. Identifies messages with only thinking blocks (no text)
|
|
22
|
+
2. Appends their thinking blocks to the parent's content_blocks
|
|
23
|
+
3. Removes the thinking-only messages from output
|
|
24
|
+
|
|
25
|
+
Original provider data is preserved in provider_data.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def transform(self, messages: List[RawMessage]) -> List[RawMessage]:
|
|
29
|
+
"""Merge thinking-only messages into parents.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
messages: Messages to transform
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Messages with thinking blocks merged
|
|
36
|
+
"""
|
|
37
|
+
if not messages:
|
|
38
|
+
return messages
|
|
39
|
+
|
|
40
|
+
# Build lookup by ID
|
|
41
|
+
msg_by_id: Dict[str, RawMessage] = {
|
|
42
|
+
m.provider_message_id: m for m in messages
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
to_remove: Set[str] = set()
|
|
46
|
+
|
|
47
|
+
for msg in messages:
|
|
48
|
+
blocks = msg.content_blocks
|
|
49
|
+
has_text = any(
|
|
50
|
+
b.get("type") == "text" and b.get("text") for b in blocks
|
|
51
|
+
)
|
|
52
|
+
has_thinking = any(b.get("type") == "thinking" for b in blocks)
|
|
53
|
+
|
|
54
|
+
# If message has thinking but no text, merge into parent
|
|
55
|
+
if has_thinking and not has_text:
|
|
56
|
+
parent_id = msg.provider_parent_id
|
|
57
|
+
if parent_id and parent_id in msg_by_id:
|
|
58
|
+
parent = msg_by_id[parent_id]
|
|
59
|
+
# Only merge into assistant messages
|
|
60
|
+
if parent.role == "assistant":
|
|
61
|
+
# Append thinking blocks to parent
|
|
62
|
+
for block in blocks:
|
|
63
|
+
if block.get("type") == "thinking":
|
|
64
|
+
parent.content_blocks.append(block)
|
|
65
|
+
to_remove.add(msg.provider_message_id)
|
|
66
|
+
|
|
67
|
+
# Remove merged messages
|
|
68
|
+
return [m for m in messages if m.provider_message_id not in to_remove]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type definitions for provider-specific export formats.
|
|
3
|
+
|
|
4
|
+
Each module defines TypedDict schemas for the raw input format from that provider.
|
|
5
|
+
These are used for type checking and IDE support when parsing exports.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .chatgpt import (
|
|
9
|
+
ChatGPTAuthor,
|
|
10
|
+
ChatGPTContent,
|
|
11
|
+
ChatGPTConversation,
|
|
12
|
+
ChatGPTExport,
|
|
13
|
+
ChatGPTMessage,
|
|
14
|
+
ChatGPTNode,
|
|
15
|
+
)
|
|
16
|
+
from .claude import (
|
|
17
|
+
ClaudeContentBlock,
|
|
18
|
+
ClaudeConversation,
|
|
19
|
+
ClaudeExport,
|
|
20
|
+
ClaudeMessage,
|
|
21
|
+
)
|
|
22
|
+
from .claude_code import (
|
|
23
|
+
ClaudeCodeExport,
|
|
24
|
+
ClaudeCodeLine,
|
|
25
|
+
ClaudeCodeMessage,
|
|
26
|
+
ClaudeCodeSession,
|
|
27
|
+
)
|
|
28
|
+
from .cursor import (
|
|
29
|
+
CursorConversation,
|
|
30
|
+
CursorExport,
|
|
31
|
+
CursorMessage,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# ChatGPT
|
|
36
|
+
"ChatGPTAuthor",
|
|
37
|
+
"ChatGPTContent",
|
|
38
|
+
"ChatGPTMessage",
|
|
39
|
+
"ChatGPTNode",
|
|
40
|
+
"ChatGPTConversation",
|
|
41
|
+
"ChatGPTExport",
|
|
42
|
+
# Claude
|
|
43
|
+
"ClaudeContentBlock",
|
|
44
|
+
"ClaudeMessage",
|
|
45
|
+
"ClaudeConversation",
|
|
46
|
+
"ClaudeExport",
|
|
47
|
+
# Claude Code
|
|
48
|
+
"ClaudeCodeMessage",
|
|
49
|
+
"ClaudeCodeLine",
|
|
50
|
+
"ClaudeCodeSession",
|
|
51
|
+
"ClaudeCodeExport",
|
|
52
|
+
# Cursor
|
|
53
|
+
"CursorMessage",
|
|
54
|
+
"CursorConversation",
|
|
55
|
+
"CursorExport",
|
|
56
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TypedDict schemas for ChatGPT export format.
|
|
3
|
+
|
|
4
|
+
Export source: ChatGPT web UI -> Settings -> Data controls -> Export data
|
|
5
|
+
File structure: ZIP archive containing conversations.json
|
|
6
|
+
|
|
7
|
+
The conversations.json contains an array of conversation objects with:
|
|
8
|
+
- mapping: Dict of message_id -> message node (tree structure)
|
|
9
|
+
- current_node: ID of the leaf node in the "active" conversation path
|
|
10
|
+
- title, create_time, update_time: Conversation metadata
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Any, Dict, List, Optional, TypedDict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChatGPTAuthor(TypedDict, total=False):
|
|
17
|
+
"""Author information for a ChatGPT message."""
|
|
18
|
+
role: str # "user", "assistant", "system", "tool"
|
|
19
|
+
name: Optional[str] # Tool/plugin name when role="tool"
|
|
20
|
+
metadata: Dict[str, Any]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChatGPTContent(TypedDict, total=False):
|
|
24
|
+
"""Content structure for a ChatGPT message.
|
|
25
|
+
|
|
26
|
+
ChatGPT uses various content_type values:
|
|
27
|
+
- "text": Regular text content
|
|
28
|
+
- "thoughts", "analysis", "reasoning_recap": Thinking blocks (o1 models)
|
|
29
|
+
- "user_editable_context", "model_editable_context": Custom instructions
|
|
30
|
+
- "code", "commentary", "metadata", "system": Structural content
|
|
31
|
+
"""
|
|
32
|
+
content_type: str
|
|
33
|
+
parts: List[Any] # Array of content parts (strings, dicts)
|
|
34
|
+
text: Optional[str] # Alternative to parts array
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ChatGPTMessageMetadata(TypedDict, total=False):
|
|
38
|
+
"""Metadata for a ChatGPT message."""
|
|
39
|
+
model_slug: Optional[str]
|
|
40
|
+
model: Optional[str]
|
|
41
|
+
is_visually_hidden_from_conversation: Optional[bool]
|
|
42
|
+
is_user_system_message: Optional[bool]
|
|
43
|
+
parent_id: Optional[str]
|
|
44
|
+
content_type: Optional[str]
|
|
45
|
+
tool_calls: Optional[List[Dict[str, Any]]]
|
|
46
|
+
tool_call: Optional[Dict[str, Any]]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ChatGPTMessage(TypedDict, total=False):
|
|
50
|
+
"""A single message in a ChatGPT conversation."""
|
|
51
|
+
id: str
|
|
52
|
+
author: ChatGPTAuthor
|
|
53
|
+
content: ChatGPTContent
|
|
54
|
+
create_time: Optional[float] # Unix timestamp
|
|
55
|
+
update_time: Optional[float] # Unix timestamp
|
|
56
|
+
metadata: ChatGPTMessageMetadata
|
|
57
|
+
status: Optional[str]
|
|
58
|
+
end_turn: Optional[bool]
|
|
59
|
+
weight: Optional[float]
|
|
60
|
+
recipient: Optional[str]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ChatGPTNode(TypedDict, total=False):
|
|
64
|
+
"""A node in the ChatGPT message tree (from mapping field)."""
|
|
65
|
+
id: str
|
|
66
|
+
parent: Optional[str] # Parent node ID
|
|
67
|
+
children: List[str] # Child node IDs
|
|
68
|
+
message: Optional[ChatGPTMessage] # None for stub nodes
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ChatGPTConversation(TypedDict, total=False):
|
|
72
|
+
"""A single ChatGPT conversation."""
|
|
73
|
+
id: str
|
|
74
|
+
title: str
|
|
75
|
+
create_time: float
|
|
76
|
+
update_time: float
|
|
77
|
+
mapping: Dict[str, ChatGPTNode] # message_id -> node
|
|
78
|
+
current_node: Optional[str] # Leaf of active path
|
|
79
|
+
moderation_results: Optional[List[Any]]
|
|
80
|
+
plugin_ids: Optional[List[str]]
|
|
81
|
+
gizmo_id: Optional[str]
|
|
82
|
+
conversation_template_id: Optional[str]
|
|
83
|
+
safe_urls: Optional[List[str]]
|
|
84
|
+
is_archived: Optional[bool]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ChatGPTUser(TypedDict, total=False):
|
|
88
|
+
"""User information from ChatGPT export."""
|
|
89
|
+
id: str
|
|
90
|
+
name: str
|
|
91
|
+
email: str
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ChatGPTExport(TypedDict, total=False):
|
|
95
|
+
"""Complete ChatGPT data export bundle."""
|
|
96
|
+
conversations: List[ChatGPTConversation]
|
|
97
|
+
user: Optional[ChatGPTUser]
|
|
98
|
+
message_feedback: Optional[Dict[str, Any]]
|
|
99
|
+
model_comparisons: Optional[List[Any]]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TypedDict schemas for Claude.ai export format.
|
|
3
|
+
|
|
4
|
+
Export source: Claude.ai -> Settings -> Account -> Export Data
|
|
5
|
+
File structure: ZIP archive containing:
|
|
6
|
+
- conversations.json - Chat conversations with messages (primary)
|
|
7
|
+
- memories.json - Claude's memory about the user (optional)
|
|
8
|
+
- projects.json - Claude Projects and attached docs (optional)
|
|
9
|
+
- users.json - Account information (optional)
|
|
10
|
+
|
|
11
|
+
Key differences from other providers:
|
|
12
|
+
- No thinking blocks (server-side only, not included in exports)
|
|
13
|
+
- No branching (strictly linear conversations)
|
|
14
|
+
- Uses "human" role where others use "user"
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import Any, Dict, List, Optional, TypedDict
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ClaudeContentBlock(TypedDict, total=False):
|
|
21
|
+
"""A content block in a Claude message.
|
|
22
|
+
|
|
23
|
+
Block types:
|
|
24
|
+
- text: Plain text content
|
|
25
|
+
- tool_use: Tool/function call
|
|
26
|
+
- tool_result: Tool/function result
|
|
27
|
+
- thinking: Extended thinking (NOT present in web exports)
|
|
28
|
+
- token_budget: Internal token tracking
|
|
29
|
+
"""
|
|
30
|
+
type: str # "text", "tool_use", "tool_result", "thinking", "token_budget"
|
|
31
|
+
text: Optional[str] # For text blocks
|
|
32
|
+
thinking: Optional[str] # For thinking blocks
|
|
33
|
+
signature: Optional[str] # For thinking blocks
|
|
34
|
+
name: Optional[str] # For tool_use and tool_result
|
|
35
|
+
input: Optional[Dict[str, Any]] # For tool_use
|
|
36
|
+
content: Optional[Any] # For tool_result
|
|
37
|
+
is_error: Optional[bool] # For tool_result
|
|
38
|
+
display_content: Optional[Any]
|
|
39
|
+
start_timestamp: Optional[str]
|
|
40
|
+
stop_timestamp: Optional[str]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ClaudeAttachment(TypedDict, total=False):
|
|
44
|
+
"""File attachment in a Claude message."""
|
|
45
|
+
id: str
|
|
46
|
+
file_name: str
|
|
47
|
+
file_type: str
|
|
48
|
+
file_size: Optional[int]
|
|
49
|
+
extracted_content: Optional[str]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ClaudeMessage(TypedDict, total=False):
|
|
53
|
+
"""A single message in a Claude conversation."""
|
|
54
|
+
uuid: str
|
|
55
|
+
text: str # Legacy field, may exist alongside content
|
|
56
|
+
content: List[ClaudeContentBlock] # Structured content blocks
|
|
57
|
+
sender: str # "human" or "assistant"
|
|
58
|
+
created_at: str # ISO timestamp
|
|
59
|
+
updated_at: Optional[str] # ISO timestamp
|
|
60
|
+
attachments: List[ClaudeAttachment]
|
|
61
|
+
files: List[Dict[str, Any]]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ClaudeAccount(TypedDict, total=False):
|
|
65
|
+
"""Account reference in a conversation."""
|
|
66
|
+
uuid: str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ClaudeProject(TypedDict, total=False):
|
|
70
|
+
"""Project reference in a conversation."""
|
|
71
|
+
uuid: str
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ClaudeConversation(TypedDict, total=False):
|
|
75
|
+
"""A single Claude conversation."""
|
|
76
|
+
uuid: str
|
|
77
|
+
name: str
|
|
78
|
+
summary: Optional[str]
|
|
79
|
+
created_at: str
|
|
80
|
+
updated_at: str
|
|
81
|
+
account: ClaudeAccount
|
|
82
|
+
project: Optional[ClaudeProject]
|
|
83
|
+
chat_messages: List[ClaudeMessage]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ClaudeMemory(TypedDict, total=False):
|
|
87
|
+
"""User memory from Claude export."""
|
|
88
|
+
content: str
|
|
89
|
+
created_at: str
|
|
90
|
+
updated_at: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ClaudeMemories(TypedDict, total=False):
|
|
94
|
+
"""Memories section of Claude export."""
|
|
95
|
+
user_memories: List[ClaudeMemory]
|
|
96
|
+
project_memories: Dict[str, str] # project_uuid -> memory text
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ClaudeProjectFull(TypedDict, total=False):
|
|
100
|
+
"""Full project information from Claude export."""
|
|
101
|
+
uuid: str
|
|
102
|
+
name: str
|
|
103
|
+
description: Optional[str]
|
|
104
|
+
created_at: str
|
|
105
|
+
updated_at: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ClaudeUser(TypedDict, total=False):
|
|
109
|
+
"""User information from Claude export."""
|
|
110
|
+
uuid: str
|
|
111
|
+
full_name: str
|
|
112
|
+
email: Optional[str]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ClaudeExport(TypedDict, total=False):
|
|
116
|
+
"""Complete Claude data export bundle."""
|
|
117
|
+
conversations: List[ClaudeConversation]
|
|
118
|
+
memories: Optional[ClaudeMemories]
|
|
119
|
+
projects: Optional[List[ClaudeProjectFull]]
|
|
120
|
+
users: Optional[List[ClaudeUser]]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TypedDict schemas for Claude Code CLI session format.
|
|
3
|
+
|
|
4
|
+
Storage location: ~/.claude/projects/<project-path>/<session-id>.jsonl
|
|
5
|
+
Where <project-path> is the working directory with / replaced by -
|
|
6
|
+
Example: /home/user/project -> -home-user-project
|
|
7
|
+
|
|
8
|
+
Sessions are JSONL (one JSON object per line) with message types:
|
|
9
|
+
- type: "user" - User messages with message.content
|
|
10
|
+
- type: "assistant" - Assistant responses with content blocks
|
|
11
|
+
- type: "system" - System events (commands, file snapshots, compaction)
|
|
12
|
+
- type: "summary" - Context continuation summaries
|
|
13
|
+
- type: "file-history-snapshot" - File state snapshots
|
|
14
|
+
|
|
15
|
+
Key characteristics:
|
|
16
|
+
- Model-specific thinking (Opus has extended thinking, Sonnet/Haiku don't)
|
|
17
|
+
- Context compaction via compact_boundary system messages
|
|
18
|
+
- IDE context embedded as XML tags in user messages
|
|
19
|
+
- Tool results in user messages (not coalesced like ChatGPT)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ClaudeCodeContentBlock(TypedDict, total=False):
|
|
26
|
+
"""A content block in a Claude Code message.
|
|
27
|
+
|
|
28
|
+
Block types mirror Claude's API:
|
|
29
|
+
- text: Plain text content
|
|
30
|
+
- thinking: Extended thinking (signature-verified)
|
|
31
|
+
- tool_use: Tool call
|
|
32
|
+
- tool_result: Tool execution result
|
|
33
|
+
"""
|
|
34
|
+
type: str # "text", "thinking", "tool_use", "tool_result"
|
|
35
|
+
text: Optional[str] # For text blocks
|
|
36
|
+
thinking: Optional[str] # For thinking blocks
|
|
37
|
+
signature: Optional[str] # For thinking blocks
|
|
38
|
+
id: Optional[str] # For tool_use
|
|
39
|
+
name: Optional[str] # For tool_use
|
|
40
|
+
input: Optional[Dict[str, Any]] # For tool_use
|
|
41
|
+
tool_use_id: Optional[str] # For tool_result
|
|
42
|
+
content: Optional[Any] # For tool_result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ClaudeCodeUsage(TypedDict, total=False):
|
|
46
|
+
"""Token usage information."""
|
|
47
|
+
input_tokens: int
|
|
48
|
+
output_tokens: int
|
|
49
|
+
cache_creation_input_tokens: Optional[int]
|
|
50
|
+
cache_read_input_tokens: Optional[int]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ClaudeCodeMessage(TypedDict, total=False):
|
|
54
|
+
"""The message object within a Claude Code line.
|
|
55
|
+
|
|
56
|
+
For user messages: content is a string or List[ContentBlock]
|
|
57
|
+
For assistant messages: content is List[ContentBlock]
|
|
58
|
+
"""
|
|
59
|
+
id: Optional[str]
|
|
60
|
+
role: str # "user" or "assistant"
|
|
61
|
+
content: Union[str, List[ClaudeCodeContentBlock]]
|
|
62
|
+
model: Optional[str]
|
|
63
|
+
stop_reason: Optional[str]
|
|
64
|
+
usage: Optional[ClaudeCodeUsage]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ClaudeCodeCompactMetadata(TypedDict, total=False):
|
|
68
|
+
"""Metadata for context compaction events."""
|
|
69
|
+
preTokens: int
|
|
70
|
+
postTokens: int
|
|
71
|
+
summarizedMessages: List[str]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ClaudeCodeLine(TypedDict, total=False):
|
|
75
|
+
"""A single line/record in a Claude Code JSONL session file.
|
|
76
|
+
|
|
77
|
+
type determines the structure:
|
|
78
|
+
- "user": User message with message.content
|
|
79
|
+
- "assistant": Assistant response with message.content blocks
|
|
80
|
+
- "system": System event (command, compaction boundary)
|
|
81
|
+
- "summary": Context continuation summary
|
|
82
|
+
- "file-history-snapshot": File state snapshot
|
|
83
|
+
"""
|
|
84
|
+
type: str # "user", "assistant", "system", "summary", "file-history-snapshot"
|
|
85
|
+
uuid: str
|
|
86
|
+
parentUuid: Optional[str]
|
|
87
|
+
sessionId: str
|
|
88
|
+
timestamp: str # ISO 8601
|
|
89
|
+
message: Optional[ClaudeCodeMessage]
|
|
90
|
+
|
|
91
|
+
# System message fields
|
|
92
|
+
subtype: Optional[str] # "compact_boundary", etc.
|
|
93
|
+
content: Optional[str] # For system messages
|
|
94
|
+
level: Optional[str] # Log level
|
|
95
|
+
|
|
96
|
+
# Context compaction fields
|
|
97
|
+
logicalParentUuid: Optional[str] # Points to pre-compaction message
|
|
98
|
+
compactMetadata: Optional[ClaudeCodeCompactMetadata]
|
|
99
|
+
|
|
100
|
+
# Summary fields
|
|
101
|
+
summary: Optional[str]
|
|
102
|
+
summarizes: Optional[List[str]]
|
|
103
|
+
summarizedMessages: Optional[List[str]]
|
|
104
|
+
|
|
105
|
+
# File snapshot fields
|
|
106
|
+
files: Optional[List[Dict[str, Any]]]
|
|
107
|
+
snapshot: Optional[Dict[str, Any]]
|
|
108
|
+
|
|
109
|
+
# Session metadata
|
|
110
|
+
cwd: Optional[str] # Working directory
|
|
111
|
+
gitBranch: Optional[str]
|
|
112
|
+
version: Optional[str] # Claude Code version
|
|
113
|
+
requestId: Optional[str]
|
|
114
|
+
|
|
115
|
+
# Branching
|
|
116
|
+
isSidechain: Optional[bool]
|
|
117
|
+
|
|
118
|
+
# Thinking metadata
|
|
119
|
+
thinkingMetadata: Optional[Dict[str, Any]]
|
|
120
|
+
|
|
121
|
+
# Todo tracking
|
|
122
|
+
todos: Optional[List[Dict[str, Any]]]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ClaudeCodeSession(TypedDict, total=False):
|
|
126
|
+
"""A parsed Claude Code session."""
|
|
127
|
+
session_id: str
|
|
128
|
+
path: Optional[str]
|
|
129
|
+
lines: List[ClaudeCodeLine]
|
|
130
|
+
parse_errors: Optional[List[Dict[str, Any]]]
|
|
131
|
+
is_subagent: Optional[bool]
|
|
132
|
+
parent_session_id: Optional[str]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class ClaudeCodeExport(TypedDict, total=False):
|
|
136
|
+
"""Claude Code export bundle (multiple sessions)."""
|
|
137
|
+
provider: Literal["claude-code"]
|
|
138
|
+
sessions: List[ClaudeCodeSession]
|
|
139
|
+
export_metadata: Optional[Dict[str, Any]]
|