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.
Files changed (132) hide show
  1. thread_archive/__init__.py +34 -0
  2. thread_archive/_api.py +2235 -0
  3. thread_archive/_config.py +126 -0
  4. thread_archive/_importers/__init__.py +81 -0
  5. thread_archive/_importers/_continuation.py +136 -0
  6. thread_archive/_importers/_cursor.py +103 -0
  7. thread_archive/_importers/_events.py +244 -0
  8. thread_archive/_importers/_line_stream.py +193 -0
  9. thread_archive/_importers/_read.py +82 -0
  10. thread_archive/_importers/_result.py +17 -0
  11. thread_archive/_importers/_sidecar.py +113 -0
  12. thread_archive/_importers/_state.py +240 -0
  13. thread_archive/_importers/_titles.py +179 -0
  14. thread_archive/_importers/antigravity.py +254 -0
  15. thread_archive/_importers/claude_code.py +293 -0
  16. thread_archive/_importers/claude_science.py +330 -0
  17. thread_archive/_importers/cloth.py +20 -0
  18. thread_archive/_importers/codex.py +324 -0
  19. thread_archive/_importers/cowork.py +107 -0
  20. thread_archive/_importers/cursor.py +432 -0
  21. thread_archive/_importers/exports.py +389 -0
  22. thread_archive/_importers/grok.py +600 -0
  23. thread_archive/_importers/opencode.py +538 -0
  24. thread_archive/_knowledge/__init__.py +67 -0
  25. thread_archive/_knowledge/_claims.py +116 -0
  26. thread_archive/_knowledge/_community.py +75 -0
  27. thread_archive/_knowledge/graph.py +208 -0
  28. thread_archive/_knowledge/materialize.py +212 -0
  29. thread_archive/_knowledge/write.py +438 -0
  30. thread_archive/_launchd.py +167 -0
  31. thread_archive/_mcp/__init__.py +1 -0
  32. thread_archive/_mcp/librarian.py +151 -0
  33. thread_archive/_mcp/server.py +307 -0
  34. thread_archive/_retrieval/__init__.py +280 -0
  35. thread_archive/_retrieval/_classify.py +80 -0
  36. thread_archive/_retrieval/_codex.py +157 -0
  37. thread_archive/_retrieval/_context.py +123 -0
  38. thread_archive/_retrieval/_extract.py +184 -0
  39. thread_archive/_retrieval/embed.py +186 -0
  40. thread_archive/_retrieval/format.py +149 -0
  41. thread_archive/_retrieval/fts.py +538 -0
  42. thread_archive/_retrieval/rank.py +228 -0
  43. thread_archive/_retrieval/read.py +908 -0
  44. thread_archive/_retrieval/rerank.py +143 -0
  45. thread_archive/_retrieval/vectors.py +611 -0
  46. thread_archive/_scripts/__init__.py +1 -0
  47. thread_archive/_scripts/backfill_recompute.py +328 -0
  48. thread_archive/_scripts/backfill_reconcile.py +296 -0
  49. thread_archive/_scripts/denamespace_dedup_keys.py +120 -0
  50. thread_archive/_scripts/recover_dropped_events.py +190 -0
  51. thread_archive/_scripts/repair_grok_tool_names.py +118 -0
  52. thread_archive/_scripts/repair_grok_tool_names_backup_20260704T155625Z.json +275 -0
  53. thread_archive/_scripts/repair_grok_tool_names_plan_20260704.json +435 -0
  54. thread_archive/_setup/__init__.py +12 -0
  55. thread_archive/_setup/clients.py +89 -0
  56. thread_archive/_setup/wizard.py +474 -0
  57. thread_archive/_store/__init__.py +45 -0
  58. thread_archive/_store/_base.py +173 -0
  59. thread_archive/_store/_defaults.py +57 -0
  60. thread_archive/_store/_types.py +38 -0
  61. thread_archive/_store/models.py +370 -0
  62. thread_archive/_store/schema.py +84 -0
  63. thread_archive/_thread_import/__init__.py +40 -0
  64. thread_archive/_thread_import/api.py +78 -0
  65. thread_archive/_thread_import/event_builder.py +810 -0
  66. thread_archive/_thread_import/exporters/__init__.py +9 -0
  67. thread_archive/_thread_import/exporters/_cursor_kv_mixin.py +166 -0
  68. thread_archive/_thread_import/exporters/_cursor_parse_mixin.py +149 -0
  69. thread_archive/_thread_import/exporters/cursor.py +582 -0
  70. thread_archive/_thread_import/exporters/cursor_parse.py +145 -0
  71. thread_archive/_thread_import/parsers/__init__.py +191 -0
  72. thread_archive/_thread_import/parsers/base.py +698 -0
  73. thread_archive/_thread_import/parsers/chatgpt.py +722 -0
  74. thread_archive/_thread_import/parsers/chatgpt_content.py +332 -0
  75. thread_archive/_thread_import/parsers/claude.py +443 -0
  76. thread_archive/_thread_import/parsers/claude_code.py +1035 -0
  77. thread_archive/_thread_import/parsers/claude_code_blocks.py +415 -0
  78. thread_archive/_thread_import/parsers/claude_code_ide.py +122 -0
  79. thread_archive/_thread_import/parsers/claude_code_sessions.py +90 -0
  80. thread_archive/_thread_import/parsers/config/__init__.py +26 -0
  81. thread_archive/_thread_import/parsers/config/base.py +220 -0
  82. thread_archive/_thread_import/parsers/cursor.py +538 -0
  83. thread_archive/_thread_import/parsers/cursor_blocks.py +365 -0
  84. thread_archive/_thread_import/parsers/pipeline/__init__.py +29 -0
  85. thread_archive/_thread_import/parsers/pipeline/base.py +251 -0
  86. thread_archive/_thread_import/parsers/pipeline/interfaces.py +191 -0
  87. thread_archive/_thread_import/parsers/transformers/__init__.py +22 -0
  88. thread_archive/_thread_import/parsers/transformers/active_path.py +87 -0
  89. thread_archive/_thread_import/parsers/transformers/coalescing.py +389 -0
  90. thread_archive/_thread_import/parsers/transformers/ide_context.py +158 -0
  91. thread_archive/_thread_import/parsers/transformers/thinking_merge.py +68 -0
  92. thread_archive/_thread_import/parsers/types/__init__.py +56 -0
  93. thread_archive/_thread_import/parsers/types/chatgpt.py +99 -0
  94. thread_archive/_thread_import/parsers/types/claude.py +120 -0
  95. thread_archive/_thread_import/parsers/types/claude_code.py +139 -0
  96. thread_archive/_thread_import/parsers/types/cursor.py +109 -0
  97. thread_archive/_thread_import/parsers/validators/__init__.py +83 -0
  98. thread_archive/_thread_import/parsers/validators/base.py +168 -0
  99. thread_archive/_thread_import/parsers/validators/content.py +80 -0
  100. thread_archive/_thread_import/parsers/validators/referential.py +57 -0
  101. thread_archive/_thread_import/parsers/validators/thinking.py +143 -0
  102. thread_archive/_thread_import/parsers/validators/types.py +87 -0
  103. thread_archive/_thread_import/schemas/__init__.py +231 -0
  104. thread_archive/_thread_import/schemas/chatgpt/v1.json +197 -0
  105. thread_archive/_thread_import/schemas/chatgpt/v2.json +203 -0
  106. thread_archive/_thread_import/schemas/claude/v1.json +188 -0
  107. thread_archive/_thread_import/schemas/claude_code/v1.json +183 -0
  108. thread_archive/_thread_import/schemas/cursor/v1.json +143 -0
  109. thread_archive/_thread_import/schemas/cursor/v2.json +200 -0
  110. thread_archive/_thread_import/timestamps.py +57 -0
  111. thread_archive/_thread_import/tool_names.py +48 -0
  112. thread_archive/_truth/__init__.py +40 -0
  113. thread_archive/_truth/jsonl_log.py +2340 -0
  114. thread_archive/_truth/repair.py +322 -0
  115. thread_archive/_watcher/__init__.py +57 -0
  116. thread_archive/_watcher/base.py +65 -0
  117. thread_archive/_watcher/daemon.py +289 -0
  118. thread_archive/_watcher/export_drop.py +203 -0
  119. thread_archive/_watcher/exthost.py +316 -0
  120. thread_archive/_watcher/lazy.py +117 -0
  121. thread_archive/_watcher/sources.py +616 -0
  122. thread_archive/_web/__init__.py +15 -0
  123. thread_archive/_web/server.py +299 -0
  124. thread_archive/_web/static/assets/index-DECSH1Mz.css +10 -0
  125. thread_archive/_web/static/assets/index-Djq0FK0y.js +101 -0
  126. thread_archive/_web/static/index.html +13 -0
  127. thread_archive/cli.py +746 -0
  128. thread_archive-0.0.2.dist-info/METADATA +296 -0
  129. thread_archive-0.0.2.dist-info/RECORD +132 -0
  130. thread_archive-0.0.2.dist-info/WHEEL +4 -0
  131. thread_archive-0.0.2.dist-info/entry_points.txt +6 -0
  132. 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]]