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,332 @@
1
+ """
2
+ Pure content-extraction / message-parsing helpers for ChatGPTParser.
3
+
4
+ These are the stateless bodies of ChatGPTParser's helper methods: content-block
5
+ building, special-case dispatch, text extraction, primary-message classification,
6
+ and coalesce-candidate collection. They carry no instance state — the parser's
7
+ methods are thin delegators over these functions. Content-block construction uses
8
+ ProviderParser's static factory methods (create_text_block, etc.) directly, so
9
+ the emitted block shapes are byte-identical to the inlined originals.
10
+
11
+ Kept in a sibling module (not on the class) purely to keep chatgpt.py lean; the
12
+ class retains every method the base interface, registry, or tests reference.
13
+ """
14
+
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ from .base import ContentBlock, ProviderParser
18
+
19
+ # Content types that indicate thinking/reasoning messages
20
+ THINKING_CONTENT_TYPES = {"thoughts", "analysis", "reasoning_recap"}
21
+
22
+ # Content types that indicate structural/scaffolding messages (not displayed)
23
+ STRUCTURAL_CONTENT_TYPES = {"code", "commentary", "metadata", "system"}
24
+
25
+ # Content types that represent system context
26
+ CONTEXT_CONTENT_TYPES = {"user_editable_context", "model_editable_context"}
27
+
28
+
29
+ def extract_text_from_content(content: Any) -> str:
30
+ """Extract text content from ChatGPT content object."""
31
+ if isinstance(content, str):
32
+ return content
33
+ if isinstance(content, dict):
34
+ # Check for text field. Only trust a string value — a dict/None `text`
35
+ # would otherwise be returned verbatim and crash downstream `.strip()`;
36
+ # fall through to the parts handling (which yields "" if there are none).
37
+ if isinstance(content.get("text"), str):
38
+ return content["text"]
39
+ # Check for parts array
40
+ parts = content.get("parts", [])
41
+ text_parts = []
42
+ for part in parts:
43
+ if isinstance(part, str):
44
+ text_parts.append(part)
45
+ elif isinstance(part, dict) and "text" in part:
46
+ text_parts.append(part["text"])
47
+ return "\n\n".join(text_parts)
48
+ return ""
49
+
50
+
51
+ def extract_special_block(
52
+ raw_msg: Dict,
53
+ content: Any,
54
+ content_type: str,
55
+ role: str,
56
+ msg_id: Optional[str],
57
+ seq: int,
58
+ ) -> Optional[tuple[List[ContentBlock], int]]:
59
+ """Handle the exclusive content cases that fully consume a message.
60
+
61
+ Returns ``(blocks, next_seq)`` when one applies, else ``None`` so the
62
+ caller continues with the additive (tool_calls/text/image) sections.
63
+ """
64
+ # Handle stub messages
65
+ if raw_msg.get("is_stub"):
66
+ return [], seq
67
+
68
+ # Handle tool role messages -> tool_result block
69
+ if role == "tool":
70
+ tool_name = raw_msg.get("author_name") or "unknown"
71
+ block = ProviderParser.create_tool_result_block(
72
+ tool_name,
73
+ content,
74
+ seq,
75
+ provider_message_id=msg_id,
76
+ )
77
+ return [block], seq + 1
78
+
79
+ # Handle thinking/reasoning content
80
+ if content_type in THINKING_CONTENT_TYPES:
81
+ text = extract_text_from_content(content)
82
+ if text:
83
+ block = ProviderParser.create_thinking_block(
84
+ text,
85
+ seq,
86
+ thinking_type=content_type,
87
+ provider_message_id=msg_id,
88
+ )
89
+ return [block], seq + 1
90
+
91
+ # Handle system context content
92
+ if content_type in CONTEXT_CONTENT_TYPES:
93
+ text = extract_text_from_content(content)
94
+ if text:
95
+ ctx_block: ContentBlock = {
96
+ "type": "system_context",
97
+ "text": text,
98
+ "context_type": content_type,
99
+ "seq": seq,
100
+ }
101
+ return [ctx_block], seq + 1
102
+
103
+ # Check for tool calls via recipient field (primary ChatGPT mechanism)
104
+ # When recipient is set to a tool name (not "all"), the assistant is invoking a tool
105
+ # and the text content is the tool input (e.g. Python code for the code interpreter)
106
+ recipient = raw_msg.get("recipient")
107
+ if recipient and recipient != "all" and role == "assistant":
108
+ text = extract_text_from_content(content)
109
+ block = ProviderParser.create_tool_use_block(
110
+ recipient,
111
+ {"code": text} if text else {},
112
+ seq,
113
+ provider_message_id=msg_id,
114
+ )
115
+ return [block], seq + 1
116
+
117
+ return None
118
+
119
+
120
+ def append_metadata_tool_calls(
121
+ blocks: List[ContentBlock],
122
+ msg_metadata: Dict,
123
+ msg_id: Optional[str],
124
+ seq: int,
125
+ ) -> int:
126
+ """Append tool_use blocks from metadata tool_calls (alternate export formats)."""
127
+ tool_calls = msg_metadata.get("tool_calls") or msg_metadata.get("tool_call")
128
+ if tool_calls:
129
+ if isinstance(tool_calls, list):
130
+ for tc in tool_calls:
131
+ if isinstance(tc, dict):
132
+ name = tc.get("name") or tc.get("function", {}).get("name") or "unknown"
133
+ args = tc.get("args") or tc.get("arguments") or tc.get("function", {}).get("arguments")
134
+ block = ProviderParser.create_tool_use_block(
135
+ name,
136
+ args,
137
+ seq,
138
+ provider_message_id=msg_id,
139
+ )
140
+ blocks.append(block)
141
+ seq += 1
142
+ elif isinstance(tool_calls, dict):
143
+ name = tool_calls.get("name") or tool_calls.get("function", {}).get("name") or "unknown"
144
+ args = tool_calls.get("args") or tool_calls.get("arguments") or tool_calls.get("function", {}).get("arguments")
145
+ block = ProviderParser.create_tool_use_block(
146
+ name,
147
+ args,
148
+ seq,
149
+ provider_message_id=msg_id,
150
+ )
151
+ blocks.append(block)
152
+ seq += 1
153
+ return seq
154
+
155
+
156
+ def append_text_content(blocks: List[ContentBlock], content: Any, seq: int) -> int:
157
+ """Append a text block from regular content, if any."""
158
+ text = extract_text_from_content(content)
159
+ if text:
160
+ block = ProviderParser.create_text_block(text, seq)
161
+ blocks.append(block)
162
+ seq += 1
163
+ return seq
164
+
165
+
166
+ def append_image_parts(blocks: List[ContentBlock], content: Any, seq: int) -> int:
167
+ """Append image blocks from multimodal content parts."""
168
+ if isinstance(content, dict):
169
+ parts = content.get("parts", [])
170
+ for part in parts:
171
+ if isinstance(part, dict):
172
+ # Image part
173
+ if part.get("content_type", "").startswith("image/"):
174
+ img_block: ContentBlock = {
175
+ "type": "image",
176
+ "asset_pointer": part.get("asset_pointer"),
177
+ "mime_type": part.get("content_type"),
178
+ "seq": seq,
179
+ }
180
+ blocks.append(img_block)
181
+ seq += 1
182
+ return seq
183
+
184
+
185
+ def extract_content_blocks(
186
+ raw_msg: Dict,
187
+ start_seq: int,
188
+ ) -> tuple[List[ContentBlock], int]:
189
+ """
190
+ Extract content blocks from a single raw message.
191
+ Returns (blocks, next_seq).
192
+ """
193
+ content = raw_msg.get("content", {})
194
+ content_type = (raw_msg.get("content_type") or "").lower()
195
+ role = (raw_msg.get("role") or "").lower()
196
+ msg_id = raw_msg.get("provider_message_id")
197
+ msg_metadata = raw_msg.get("msg_metadata", {})
198
+
199
+ # Exclusive cases (stub / tool role / thinking / system context /
200
+ # recipient tool call) each fully handle the message and return early.
201
+ special = extract_special_block(
202
+ raw_msg, content, content_type, role, msg_id, start_seq
203
+ )
204
+ if special is not None:
205
+ return special
206
+
207
+ # Additive sections: a message can carry any combination of metadata
208
+ # tool calls, regular text, and multimodal images, appended in that order.
209
+ blocks: List[ContentBlock] = []
210
+ seq = start_seq
211
+ seq = append_metadata_tool_calls(blocks, msg_metadata, msg_id, seq)
212
+ seq = append_text_content(blocks, content, seq)
213
+ seq = append_image_parts(blocks, content, seq)
214
+ return blocks, seq
215
+
216
+
217
+ def build_content_blocks(
218
+ primary_msg: Dict,
219
+ coalesced_msgs: List[Dict],
220
+ ) -> List[ContentBlock]:
221
+ """
222
+ Build content_blocks array from primary message and coalesced children.
223
+ """
224
+ blocks: List[ContentBlock] = []
225
+ seq = 0
226
+
227
+ # Extract content from primary message
228
+ primary_blocks, seq = extract_content_blocks(primary_msg, seq)
229
+ blocks.extend(primary_blocks)
230
+
231
+ # Add blocks from coalesced messages
232
+ for child_msg in coalesced_msgs:
233
+ child_blocks, seq = extract_content_blocks(child_msg, seq)
234
+ blocks.extend(child_blocks)
235
+
236
+ return blocks
237
+
238
+
239
+ def is_primary_message(raw_msg: Dict) -> bool:
240
+ """
241
+ Determine if a message should be a primary row (vs coalesced into parent).
242
+
243
+ Primary messages:
244
+ - User messages with displayable content
245
+ - Assistant messages with text content
246
+ - System messages
247
+
248
+ Non-primary (coalesce candidates):
249
+ - Tool role messages
250
+ - Thinking/reasoning messages (content_type in THINKING_CONTENT_TYPES)
251
+ - Stubs
252
+ """
253
+ if raw_msg.get("is_stub"):
254
+ return False
255
+
256
+ role = (raw_msg.get("role") or "").lower()
257
+ content_type = (raw_msg.get("content_type") or "").lower()
258
+
259
+ # Tool messages are coalesced
260
+ if role == "tool":
261
+ return False
262
+
263
+ # Thinking/reasoning messages are coalesced
264
+ if content_type in THINKING_CONTENT_TYPES:
265
+ return False
266
+
267
+ # System context is coalesced
268
+ if content_type in CONTEXT_CONTENT_TYPES:
269
+ return False
270
+
271
+ # Structural content is coalesced
272
+ if content_type in STRUCTURAL_CONTENT_TYPES:
273
+ return False
274
+
275
+ # Check for actual displayable content
276
+ content = raw_msg.get("content", {})
277
+ if isinstance(content, dict):
278
+ parts = content.get("parts", [])
279
+ text = content.get("text", "")
280
+ if not parts and not text:
281
+ # No content to display
282
+ return False
283
+
284
+ return role in ("user", "assistant", "system")
285
+
286
+
287
+ def collect_coalesce_candidates(
288
+ parent_id: str,
289
+ children_by_parent: Dict[str, List[str]],
290
+ msg_by_id: Dict[str, Dict],
291
+ active_path_ids: Any,
292
+ ) -> List[str]:
293
+ """
294
+ Recursively collect child messages that should be coalesced into parent.
295
+
296
+ Only coalesce messages on the active path that are:
297
+ - Tool messages
298
+ - Thinking/reasoning messages
299
+
300
+ Stop recursion at branch points (multiple children on different paths).
301
+ """
302
+ result: List[str] = []
303
+ to_process = list(children_by_parent.get(parent_id, []))
304
+
305
+ while to_process:
306
+ child_id = to_process.pop(0)
307
+ child_msg = msg_by_id.get(child_id)
308
+ if not child_msg:
309
+ continue
310
+
311
+ # Only coalesce messages on the active path
312
+ if child_id not in active_path_ids:
313
+ continue
314
+
315
+ # Check if this is a coalesce candidate
316
+ role = (child_msg.get("role") or "").lower()
317
+ content_type = (child_msg.get("content_type") or "").lower()
318
+
319
+ should_coalesce = (
320
+ role == "tool" or
321
+ content_type in THINKING_CONTENT_TYPES or
322
+ content_type in CONTEXT_CONTENT_TYPES or
323
+ content_type in STRUCTURAL_CONTENT_TYPES or
324
+ child_msg.get("is_stub")
325
+ )
326
+
327
+ if should_coalesce:
328
+ result.append(child_id)
329
+ # Continue to this message's children
330
+ to_process.extend(children_by_parent.get(child_id, []))
331
+
332
+ return result