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,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
|
+
...
|