Local-first conversation reconstruction

Find the conversation.

Ariadne combines X/Twitter archives and tweet datasets, follows the reply and quote IDs they contain, and renders root-to-target branches for reading, retrieval, or model input.

uv tool install ariadne-xariadne interactive
Local first
ordinary archive builds stay offline
Cross-archive
one branch can span several datasets
Five formats
messages, OpenAI, JSON, Markdown, Raft
On this page

01 · Quickstart

Go from an archive to readable branches.

Use a one-shot build when you have a personal export and want an answer now. Nothing is imported into Ariadne's persistent library.

What reconstruction means

Ariadne follows the parent IDs your sources know about. Missing posts remain visible as unavailable placeholders; use --strict when you would rather fail than keep a partial branch.

1. Install the command

Terminal
uv tool install ariadne-x
ariadne --help

2. Build Markdown from a personal archive

Terminal
ariadne build \
  --archive ~/Downloads/twitter-archive.zip \
  --for-user alice \
  --since 2024-01-01 \
  --format markdown \
  --output conversations.md

Replace alice with the archive owner's username. The file contains one retained root-to-target branch per section, including attribution, quote context, warnings, and placeholders where a post could not be resolved.

Prefer prompts?

Run ariadne interactive for a guided conversation build. The local dump explorer is a different command: ariadne dumps interactive.

02 · Choose an input

Start with the data you already have.

One archive

Build without importing

--archive PATH

Use a personal export or Community Archive folder, ZIP, or data file once.

Reusable collection

Import into the library

ariadne dumps import PATH

Normalize and index archives you want to search or combine repeatedly.

CSV / JSON / JSONL

Load generic records

--tweets-file PATH

Fields survive when the source provides them; sparse input may not contain reply edges.

Known posts

Pass IDs or X URLs

ariadne build [ITEM ...]

Combine positional items with local sources or an explicitly enabled API fallback.

Public X account

Try cheap network sources

--target-user USER

Enables unofficial RSS and oEmbed by default. It is convenient, not a completeness guarantee.

Bluesky

Use its public thread API

ariadne bluesky HANDLE

No authentication is required; output uses the same five renderers.

Install as a library

Terminal
python3 -m pip install ariadne-x

# Add DuckDB only for Parquet imports
uv tool install 'ariadne-x[parquet]'

Ariadne requires Python 3.11 or newer. The distribution is named ariadne-x; both the command and import package are named ariadne. Personal archives, Community CSV/ZIP, and generic files work without the Parquet extra.

03 · How it works

Target to root for lookup. Root to target for output.

  1. 01LoadNormalize archives, files, dumps, and cache.
  2. 02SelectChoose IDs or matching user posts as targets.
  3. 03FollowWalk each known reply-parent chain toward its root.
  4. 04AttachAdd quote paths when the selected sources provide them.
  5. 05RenderPrune subset branches and write root-to-target output.

Branches, not whole trees

A target includes its ancestors. Sibling replies are not discovered or appended.

Dates select targets

--since does not discard older ancestors needed by a selected branch.

Partial data stays visible

Unresolved posts produce warnings and placeholders unless --strict is set.

Quotes have two jobs

Quote context is attached separately. A root quote-tweet is spliced onto its quoted post by default.

Useful controls

The default ancestor limit is 50. Use --max-depth, --no-quotes, or --no-quote-as-reply to change the reconstruction policy.

04 · Archive library

Import once, then search and combine locally.

A persistent import is a normalized SQLite database under ~/.ariadne/dumps, or $ARIADNE_HOME/dumps. Later queries no longer need the source. Removing an import never removes or edits that source.

Terminal
ariadne dumps import ~/Downloads/twitter-archive.zip --name personal
ariadne dumps import ~/DATA/community.zip --name community
ariadne dumps list

ariadne dumps search 'remembered phrase' --user alice
ariadne dumps user alice --since 2024-01-01 --limit 50
ariadne dumps show https://x.com/alice/status/1234567890123456789
Supported persistent import shapes
KindAccepted inputExplicit flag
Personal X/Twitter archivefolder, ZIP, or archive data file--kind twitter-archive
Community Archive exportCSV directory or ZIP--kind community-csv
Parquet collectionone .parquet file or a directory--kind parquet
Generic tweet dumpCSV, JSON, JSONL, or NDJSON--kind tweets-file
  • All imported dumps participate in a build automatically. Repeat --dump NAME to narrow the set, or use --no-dumps for an isolated run.
  • Duplicate post IDs are merged across selected dumps, preserving complementary metadata.
  • --no-fts skips the full-text index; search then falls back to slower substring matching.
  • More than 10,000 matching local posts requires a narrower --since or explicit --dump-limit.
Read the complete archive library guide

05 · Network sources

Know which sources have text and which have edges.

A reply branch can continue only when a source knows the parent post ID. Text-only sources can repair content, but they cannot infer the missing edge. Ordinary local builds do not make network requests.

Source capabilities and activation
SourceFinds targetsReply IDsEnabled by
Local archives and dumpsyes, when presentyes, when presentAutomatic
Community Archivedonor accountsyes--community-archive
oEmbednono; content only--oembed or --target-user
Nitter / XCancel RSSrecent candidatesusually no--unofficial-rss or --target-user
twitterapi.ioyesyesAPI key; potentially paid
X API v2yes with timeline fetchyesBearer token; potentially billable

Public-account convenience mode

--target-user alice tries the local store and cache, then the default Nitter/XCancel-style RSS endpoints and oEmbed. Those public services are unsupported, fragile, commonly recent-only, and usually lack parent IDs. Disable them with--no-unofficial-rss --no-oembed.

Terminal
# Permit official X API reads after cheap sources
ariadne build \
  --target-user alice \
  --fetch \
  --fetch-user-timeline \
  --max-user-pages 2 \
  --format raft \
  --output alice.jsonl

--fetch resolves selected posts and missing parents; --fetch-user-timelineenumerates the user timeline. Both require an X bearer token and may consume billable API reads. Community Archive is donor-scoped; twitterapi.io is a separate potentially paid gateway.

Read the full source policy

06 · Output formats

Choose the representation your next step needs.

Ariadne renderers
FormatEncodingBest for
messagesJSONEnriched chat-like messages with tweet metadata. This is the CLI default.
openaiJSONThe same conversation envelope, with each nested message reduced to role, name, and content.
jsonJSONNormalized tweets, root-to-target branches, quote paths, warnings, and provenance.
markdownTextA readable view with attribution, unavailable placeholders, quotes, and warnings.
raftJSONLOne retrieval document per retained branch, ready for chunking or embedding.
Terminal
# Read it
ariadne build --archive archive.zip --for-user alice --format markdown

# Feed retrieval
ariadne build --archive archive.zip --for-user alice --format raft -o branches.jsonl

# Keep the normalized graph
ariadne build --archive archive.zip --for-user alice --format json -o graph.json
Role names describe position, not speaker intent

In messages and openai, the root is assistant and later posts are user. In raft, later posts are participant.

Inspect the output schemas

07 · Python API

Run the same pipeline without a file round-trip.

The public API is typed and synchronous. Pass keywords for compact calls, or use BuildOptions when configuration should be reusable and inspectable.

Python
from pathlib import Path
import ariadne

options = ariadne.BuildOptions(
    archive=Path("twitter-archive.zip"),
    for_user="alice",
    since="2024-01-01",
    no_dumps=True,  # keep this run isolated from persistent imports
)

result = ariadne.build(options)

for conversation in result:
    print(conversation.target_id)

documents = result.raft_documents()
result.save("out/branches.jsonl", "raft")
result.conversationsReconstructed branch objects
result.tweetsNormalized tweets used by the result
result.warningsProvider and data-quality diagnostics
result.render(format)Any renderer as text
result.messages()Parsed enriched or strict messages
result.json_payload()Parsed graph payload

Handle stable API errors

Python
try:
    result = ariadne.build(for_user="alice")
except ariadne.NoTargetsError:
    print("No posts matched the target selection")
except ariadne.AriadneError as exc:
    print(f"Ariadne could not build: {exc}")

Imported dumps are used automatically unless no_dumps=True. Multi-value options accept one value or an iterable, filesystem inputs accept PathLike, and credentials are excluded from option representations. In async applications, run build in a worker thread.

Read the complete Python API reference

08 · Troubleshooting

Common problems have explicit fixes.

No posts matched

Supply a tweet ID/URL or a target selector such as --for-user, --author-id, or --all-loaded. Check that --since is not too narrow.

A parent is unavailable

None of the selected sources resolved that ID. Add a richer archive or explicitly enable a structured network source. Use --strict only if partial branches should fail.

A local timeline exceeds 10,000 posts

Add a narrower --since date or an explicit --dump-limit. Explicit limits keep the newest matching posts.

A Parquet import asks for DuckDB

Install the optional dependency with uv tool install 'ariadne-x[parquet]'.

Search is unexpectedly slow

The import may have been created with --no-fts. Re-import it without that flag to build the full-text index.

Unrelated imported posts appear in a build

Persistent dumps join builds by default. Restrict them with repeatable --dump NAME, or disable them with --no-dumps.

Reference and development

Contributing
uv sync --extra dev --extra parquet
uv run pytest
uv run ruff check .
uv run mypy src