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,84 @@
1
+ """Schema provisioning for the SQLite store.
2
+
3
+ ``init_db`` is the schema baseline: ``Base.metadata.create_all`` over the active
4
+ (or given) engine, then ``_add_missing_columns``. Idempotent — existing tables are
5
+ left as-is. There is no alembic; the JSONL truth log + ``reindex`` are the recovery
6
+ primitive.
7
+
8
+ ``create_all`` only issues ``CREATE TABLE`` for tables that don't exist yet, so it
9
+ never adds anything to a table a live index already has. Columns are the one gap that
10
+ can't wait for a reindex: the ORM selects every mapped column, so an index missing a
11
+ newly-added one fails *every* query against that table. Those are ALTERed in on open.
12
+
13
+ Missing *indexes* are left alone here — they're a correctness/performance property of
14
+ the data, not something a query needs to parse, and building one (e.g. the unique
15
+ dedup index over a multi-million-row events table) is far too much work to do silently
16
+ inside an open. ``verify`` reports an under-enforced schema and ``reindex`` heals it;
17
+ that stays the operator-visible path.
18
+
19
+ The FTS5 / vector *virtual* tables are not created here — they are built on
20
+ demand by the search layer's ``ensure_index``, since they are derived
21
+ projections, not base tables.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ import threading
28
+
29
+ from sqlalchemy import text
30
+ from sqlalchemy.engine import Engine
31
+ from sqlalchemy.exc import OperationalError
32
+
33
+ from . import models # noqa: F401 (import for side effect: registers tables on Base.metadata)
34
+ from ._base import Base, get_engine
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ # table → {column: SQLite type declaration}. Columns added to a table after its
39
+ # baseline shipped; ALTERed in on open when a live index predates them.
40
+ _ADDED_COLUMNS: dict[str, dict[str, str]] = {
41
+ "import_state": {"last_content_hash": "TEXT"},
42
+ }
43
+
44
+
45
+ def _add_missing_columns(engine: Engine) -> None:
46
+ with engine.begin() as conn:
47
+ for table, columns in _ADDED_COLUMNS.items():
48
+ present = conn.execute(
49
+ text("SELECT name FROM sqlite_master WHERE type='table' AND name=:t"),
50
+ {"t": table},
51
+ ).first()
52
+ if not present:
53
+ continue
54
+ have = {row[1] for row in conn.execute(text(f"PRAGMA table_info({table})"))}
55
+ for column, decl in columns.items():
56
+ if column in have:
57
+ continue
58
+ conn.execute(text(f"ALTER TABLE {table} ADD COLUMN {column} {decl}"))
59
+ logger.info("schema: added %s.%s (%s)", table, column, decl)
60
+
61
+
62
+ # Concurrent openers race on a virgin store: ``create_all``'s existence check is
63
+ # not atomic with its CREATEs, so two threads (e.g. the MCP server's warm-models
64
+ # search vs. the first tool call) can both see "no tables" and collide. The lock
65
+ # serializes openers in this process; the retry absorbs a racer in another one.
66
+ _init_lock = threading.Lock()
67
+
68
+
69
+ def init_db(engine: Engine | None = None) -> None:
70
+ """Create all base tables on ``engine`` (or the active engine), then ALTER in any
71
+ column a pre-existing index predates. Idempotent and safe under concurrent
72
+ first-open: a lost CREATE race is retried, not raised."""
73
+ engine = engine or get_engine()
74
+ with _init_lock:
75
+ for attempt in (1, 2, 3):
76
+ try:
77
+ Base.metadata.create_all(engine)
78
+ break
79
+ except OperationalError as exc:
80
+ if "already exists" not in str(exc) or attempt == 3:
81
+ raise
82
+ # Another process created it between check and CREATE; re-run —
83
+ # create_all skips what now exists and creates the remainder.
84
+ _add_missing_columns(engine)
@@ -0,0 +1,40 @@
1
+ """
2
+ Thread Import Package
3
+
4
+ Private to thread_archive (hence the underscore): a vendored, dependency-free
5
+ parsing island. Not part of the package's public API until deliberately exposed.
6
+
7
+ Parsing library for importing conversations from AI services into Thread:
8
+ - Claude Code: Direct from ~/.claude/projects/*.jsonl
9
+ - Cursor: Direct from state.vscdb
10
+ - ChatGPT/Claude Web: From export ZIPs
11
+
12
+ Architecture:
13
+ - Parsers: Convert provider-specific formats to NormalizedMessage
14
+ - EventBuilder: Converts NormalizedMessage to Thread events
15
+ - Exporters: Extract chat data from local storage (e.g., Cursor's SQLite)
16
+ """
17
+
18
+ from .api import ConversationMeta, ImportSource
19
+ from .event_builder import DefaultEventBuilder, EventBuilder, ThreadEvent
20
+ from .exporters import CursorExporter
21
+ from .parsers import ChatGPTParser, ClaudeCodeParser, ClaudeParser, ProviderParser, get_parser
22
+
23
+ __all__ = [
24
+ # API
25
+ "ImportSource",
26
+ "ConversationMeta",
27
+ # Event Building
28
+ "EventBuilder",
29
+ "DefaultEventBuilder",
30
+ "ThreadEvent",
31
+ # Parsers
32
+ "ChatGPTParser",
33
+ "ClaudeParser",
34
+ "ClaudeCodeParser",
35
+ "ProviderParser",
36
+ "get_parser",
37
+ # Exporters
38
+ "CursorExporter",
39
+ ]
40
+
@@ -0,0 +1,78 @@
1
+ """Public API for thread_archive._thread_import.
2
+
3
+ This module defines the interfaces that consumers (like backend/importer.py)
4
+ should use to access archive data. The key principle is that consumers never
5
+ touch the archive database directly - they go through ImportSource.
6
+
7
+ This creates a clean boundary:
8
+ - Archive internals can change without breaking consumers
9
+ - Consumers can be tested with mock ImportSource implementations
10
+ - Coalescing and normalization happen in one place (the source)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from datetime import datetime
17
+ from typing import Iterator, Optional, Protocol
18
+
19
+ from .parsers.base import NormalizedMessage
20
+
21
+
22
+ @dataclass
23
+ class ConversationMeta:
24
+ """Metadata about a conversation for import decisions.
25
+
26
+ This is what ImportSource.list_conversations() yields.
27
+ Contains enough info to decide whether to import without
28
+ loading all messages.
29
+ """
30
+ source_provider: str
31
+ provider_conversation_id: str
32
+ title: Optional[str] = None
33
+ message_count: int = 0
34
+ created_at: Optional[datetime] = None
35
+ updated_at: Optional[datetime] = None
36
+
37
+
38
+ class ImportSource(Protocol):
39
+ """Protocol for anything that can produce messages for import.
40
+
41
+ Implementations:
42
+ - DirectParseSource: Parses export files directly
43
+ - InMemorySource: For testing
44
+ """
45
+
46
+ def list_conversations(
47
+ self,
48
+ provider_filter: Optional[str] = None,
49
+ limit: Optional[int] = None,
50
+ ) -> Iterator[ConversationMeta]:
51
+ """List available conversations.
52
+
53
+ Args:
54
+ provider_filter: Only include this provider (e.g., "claude-code")
55
+ limit: Maximum number of conversations to return
56
+
57
+ Yields:
58
+ ConversationMeta for each conversation
59
+ """
60
+ ...
61
+
62
+ def get_messages(
63
+ self,
64
+ provider: str,
65
+ conversation_id: str,
66
+ coalesce: bool = True,
67
+ ) -> Iterator[NormalizedMessage]:
68
+ """Get messages for a conversation.
69
+
70
+ Args:
71
+ provider: Source provider name
72
+ conversation_id: Provider's conversation ID
73
+ coalesce: Whether to coalesce consecutive assistant messages
74
+
75
+ Yields:
76
+ NormalizedMessage objects in order, ready for event creation
77
+ """
78
+ ...