| name | x-to-skill |
| description | Collects a person's public X/Twitter posts into a provenance-annotated corpus and converts their recurring ideas into a structured agent skill. Use when the user wants to learn from a specific account ("turn @karpathy into a skill", "extract X's thinking"), study a practitioner who publishes on X rather than in books, or build a citable knowledge base from someone's posts. |
X-to-Skill Converter
Turn an account's public posting history into a toolkit of their frameworks —
with a URL behind every claim.
Requirement: opencli
Hard dependency. opencli is the only sanctioned way this skill reads x.com.
It drives a real browser reusing your existing Chrome login session, so there are
no API keys and no credentials in this repo.
npm install -g @jackwener/opencli
opencli twitter whoami -f json
scripts/collect.py aborts with install instructions if opencli is missing.
Do not substitute a scraper or an API client. The safety guards in Step 0
are built on opencli's output shape (author per post, whoami); a different
backend silently voids them.
Optional: agent-reach doctor --json reports backend health if that skill is
installed. Not required — opencli twitter whoami is the direct check.
Invocation
| Host | How |
|---|
| OpenCode | /x-to-skill https://x.com/<user> |
| Claude Code, Copilot CLI, Amp, other | "run x-to-skill on https://x.com/\<user>" |
| Shell only | python3 scripts/collect.py https://x.com/<user>, then Steps 3–4 by hand |
Every host accepts a full URL or a bare @handle. The slash command is an
OpenCode convenience wrapper around the same steps — nothing below depends on it.
Philosophy
A book is edited, ordered, and argued once. A timeline is none of those things:
it is thousands of fragments, written across years, in which the author changed
their mind, joked, retweeted others, and occasionally published something that
would have been a chapter. The value is real but it is unsorted and unsigned.
So this skill inverts book-to-skill's problem. There, structure exists and must
be extracted. Here, structure must be discovered — and the main risk is not
missing an idea, it is inventing one.
Three rules follow:
Every claim cites a post. A framework attributed to the author without a
post URL is fabrication. If you cannot point to the post, drop the claim.
Repetition is the signal. In a book, one mention makes a chapter. In a
timeline, one mention makes a passing thought. An idea earns a place in the
skill when the author returns to it — across multiple posts, ideally across
months. A single viral post is a data point, not a framework.
Quoted words are not the author's words. Retweets are excluded outright.
Quote-tweets keep the quoted block only as context for the author's reply.
Never attribute the quoted text to the author.
Modes of Operation
1. Full Conversion (Default)
Trigger: User provides an @handle or x.com URL
Action: Steps 0–4
Output: Local corpus directory + generated skill
2. Collect Only
Trigger: "just collect", "get the posts", "I want the corpus"
Action: Steps 0–2. Stop.
Output: <username>-corpus/ with manifest, raw source, compact corpus,
sample, and monthly chunks
3. Build From Existing Corpus
Trigger: User points at a prior <username>-corpus/
Action: Skip collection. Verify manifest.json, start from sample.md, then
read only the monthly chunks relevant to candidate themes.
Output: Generated or refreshed skill; no X request, no re-ingestion bill
4. Refresh (Existing Skill)
Trigger: User wants posts newer than an existing corpus window
Action: Collect only the uncovered date window into a separate corpus
snapshot, analyze it, then fold verified changes into the skill. Preserve the
old corpus — never overwrite provenance.
Output: Updated skill + new dated corpus snapshot.
Step 0 — Out-of-scope check
If no handle is provided, stop:
"x-to-skill requires an X handle or URL, e.g. https://x.com/<user> or @<user>."
What this skill reads — and what it never reads
Reads: the public posts of the named account, and nothing else.
Never reads: the operator's own account. Not their timeline, DMs, bookmarks,
notifications, likes, or following list. OpenCLI runs on the operator's logged-in
browser session, so the operator's own data is technically within reach — that is
precisely why the boundary is enforced in code, not left to intent:
parse_handle() aborts on an empty or unparseable handle. It never falls
through to a bare opencli twitter tweets, which silently returns the
logged-in user's timeline.
main() compares the target against opencli twitter whoami and refuses
when they match, unless --allow-self is passed explicitly.
fetch() verifies every returned post is authored by the requested handle and
aborts if anything else appears.
Run python3 scripts/test_collect.py to verify these guards (10 accepted forms,
12 rejected).
The target account must be public. The session is used to read x.com as a
logged-in browser would; it is not a key to protected accounts. If the account is
protected, collection returns nothing — report that and stop. Do not attempt
another auth path, and never touch, rotate, or inspect the session credentials.
Refuse and explain when
- The target is protected or private. Only public posts are in scope.
- The target is a private individual with no public teaching intent. This
skill is for practitioners who publish ideas — not for profiling people.
- The user wants a dossier on someone. Building a behavioral profile of a
person is out of scope regardless of how the request is phrased.
- The request is to imitate someone's voice to post as them. Extracting how
someone thinks is fine; manufacturing their persona is not.
Step 1 — Verify the backend
opencli twitter whoami -f json
Two things must hold before collecting:
opencli is installed. If not, stop and give the install line from
Requirement: opencli above. collect.py enforces this too, but failing here
is cheaper than failing mid-run.
- The session is logged in. If
logged_in is false, stop and ask the
operator to log in to x.com in Chrome. Do not attempt another auth path and do
not scrape x.com directly.
Note the username it returns — that is the operator's own account, and Step 0's
self-guard refuses to collect it.
Never touch, rotate, or inspect the session credentials themselves.
(agent-reach doctor --json gives the same signal plus other platforms, if that
skill happens to be installed. Optional.)
Step 1.5 — Intake
Ask these questions in one turn before collecting. Defaults let a user skip
choices without blocking the workflow.
| Question | Choices | Default | What it changes |
|---|
| Profile type | Technical · Philosophical · Idealistic · Mixed / custom | Mixed / custom | The analytical lens and candidate-theme vocabulary — never the collected posts |
| Time window | Last 3 · 6 · 12 · 24 months · all available · custom dates | Default: last 12 months | --since / --until and expected collection size |
| Purpose and depth | Reference economic · Study deep | Reference economic | Whether analysis stays at sample.md + selective chunks, or reads all relevant chunks |
Profile type is a lens, not a label or a restriction. A Karpathy corpus may
be technical; a Cortella corpus may be philosophical; either can contain the
other themes. Use the type to form hypotheses, then prove or discard them from
posts. For Mixed / custom, ask for one sentence about what the user wants to
learn, e.g. "management principles" or "faith and ethics."
Time window is a request, not a promise. The collector records both requested
and actual windows in manifest.json; X pagination can return less history than
asked. Never represent the actual window as complete history.
Intake examples
/x-to-skill https://x.com/karpathy
→ technical · last 12 months · reference economic
/x-to-skill https://x.com/<cortella-handle>
→ philosophical · last 24 months · study deep
Step 2 — Collect the corpus
python3 scripts/collect.py https://x.com/<user> --since 2025-08-26 --until 2026-08-26 \
--limit 400 -o <user>-2025-08-26_2026-08-26
The argument may be a full URL or a bare @handle — parse_handle() accepts
x.com, twitter.com, www./mobile. prefixes, trailing slashes, query
strings, and /status/... subpaths. It aborts on anything else rather than
guessing.
If the user takes the defaults, omit dates: the collector uses the prior 365
days through today. --limit caps fetched posts, not kept posts. The date
filter is inclusive and local, so fetch enough to cover the requested period.
| Request | Command shape | Cost implication |
|---|
| Default: 12 months | --limit 400 | Good first pass for active accounts |
| Short recent view | --since YYYY-MM-DD --limit 200 | Fewer fetched posts / chunks |
| 24 months | --since YYYY-MM-DD --limit 800 | More history; collect once, reuse corpus |
| All available | omit --since, raise --limit deliberately | X may still stop short; can be expensive |
| Exact custom range | both --since and --until | Bounded and reproducible |
The collector excludes retweets, filters by date, and writes <outdir>/:
| File | Purpose | Read by default? |
|---|
manifest.json | Counts, requested/actual windows, format, chunk list, sample scoring | Yes |
raw.jsonl | Canonical structured source; quoted external text appears only here | No |
sample.md | Discovery sample: up to 3 posts/month, globally capped at 18,000 chars | Yes |
chunks/YYYY-MM.md | Canonical rendered source, one compact monthly slice; a heavy month splits into ordered parts YYYY-MMa, YYYY-MMb, … | Only when a candidate theme needs evidence |
corpus.md | Optional one-file export, emitted only with --full-corpus | Never by default |
Use --format txt for plain text, --chunk none only for a tiny corpus, and
--sample-per-month N only when the default sample is too thin.
--chunk-max-bytes N caps a chunk file (default: 40,000). A prolific month is
split chronologically into ordered parts rather than becoming one oversized file
— a monthly chunk stops being a cost boundary the moment it exceeds the budget.
Read chunk_index in the manifest for each month's ordered parts; filename sort
is wrong past z (2026-08aa sorts before 2026-08b).
--sample-max-chars N controls the full discovery budget (default: 18,000). A
candidate that would exceed it is skipped, never truncated; the manifest records
the requested and actual sample size. Use --full-corpus only when a portable
one-file export is explicitly needed. Every block has a date and URL; quote-tweets
identify the other author but never reproduce their text outside raw.jsonl.
Read manifest.json before continuing. It is the evidence of what was
actually obtained. A skill must name that actual window — never claim to describe
"how the author thinks" beyond it.
X's timeline API can stop well short of full history, and a prolific account
exhausts --limit long before the requested period. When the delivered window
covers less than 40% of the request, the collector prints a warning and records
window_warning in the manifest; a null there means the check ran and passed.
Never treat a collapsed window as the requested one — see Handle a collapsed
window before generating in Step 3.
Token economy — collect once, read selectively
A corpus is a local source artifact, like an extracted book. The expensive
network collection happens once; reasoning reads only the evidence needed for
the chosen depth. Reuse it instead of collecting again for another skill,
question, or refinement.
Reference economic (default):
- Read
manifest.json — establish counts and actual window.
- Read
sample.md — up to 3 posts/month, selected by deterministic local
signal score and capped at 18,000 chars across the full sample.
- Form candidate themes from that sample and the chosen profile lens.
- For each candidate, only then read the relevant
chunks/YYYY-MM.md files to
count recurrence, check spread, recover exact language, and test for
contradiction.
- Open
raw.jsonl only when quote context or full structured source data is
necessary.
Do not load a full corpus by default. Monthly chunks are the rendered source
and cost-control boundary. corpus.md exists only if the user explicitly passed
--full-corpus; it is a portability export, never a second canonical source.
Study deep: start the same way, then read all monthly chunks that plausibly
contain the final themes. Do not treat "deep" as permission to ignore the actual
window or provenance.
The output stays book-to-skill shaped — structured patterns, not a timeline
summary — but the corpus replaces a book's extracted text and the sample/chunks
replace a blind full-context read.
Reuse a snapshot instead of re-collecting
python3 scripts/collect.py https://x.com/<user> --reuse <user>-corpus
--reuse answers from an existing local snapshot and never contacts X. It loads
that directory's manifest.json and refuses unless the snapshot answers exactly
this request: same collector schema, same target, same requested window, same
format, chunking, and sample settings, with raw.jsonl present and parseable.
Any mismatch names the offending field and stops — a near-miss snapshot is a
different corpus, and silently reusing it would answer the question from the
wrong data.
manifest.json records a collection_key: a deterministic hash of the request
(schema, target, window, limit, format, chunking, sample settings). Equal keys
mean equal requests, never a guarantee that X returned identical or complete
history. The actual window remains the honest scope.
Report what was actually spent
Every run prints, and the manifest records, measured figures rather than
estimates of money:
economy.x_fetches_this_run and posts_fetched_this_run — 0 on reuse
measured_bytes — real source, chunk, and sample sizes on disk
token_estimates.reference_path — sample plus one average chunk, the expected
cost of the default reading path
token_estimates.all_chunks — what a full chunk sweep would cost instead
State these when reporting to the user. Do not convert them into currency: the
collector uses a browser session, so there is no per-request price to quote.
Step 3 — Find what repeats
This is the step with no book-to-skill equivalent, and the one that decides
whether the output is a skill or a scrapbook.
Read the sample first, then cluster posts by recurring idea. Use selective monthly
chunks to prove each candidate. For each candidate:
- Count the posts, against the window's bar. The threshold is not fixed —
it scales with how much history you actually have. See Calibrate the
recurrence bar below. Below the bar → observation, or drop it.
- Check the spread. Three posts in one week is a mood; three posts across
a year is a conviction. Prefer themes the author returns to.
- Take the author's own words. If they named the idea, keep the name
exactly — the same precision rule as book-to-skill ("The 5 Whys" is not
"ask why a few times").
- Record every source URL. These become the citations in the final skill.
- Log the contradictions. People change their minds in public, and the
timeline preserves both sides. When early and late posts disagree, keep
both, dated. An evolving position is more useful than a flattened one.
- Record the evidence files. A claim's URLs tell what supports it; list
the monthly chunks read so another agent can reproduce the route without
loading the whole corpus.
Produce a clustering report before generating anything:
## Theme: <name in the author's words>
Posts: <N> across <first date> – <last date>
Sources: <url>, <url>, <url>
Evidence files: sample.md; chunks/YYYY-MM.md, chunks/YYYY-MM.md
Claim: <one sentence the posts actually support>
Evolution: <only if the position visibly changed>
Discard themes that cannot fill this template honestly.
Calibrate the recurrence bar to the actual window
"Three posts" only means conviction when there was enough time for the author to
return to the idea. Read actual_window from manifest.json and apply:
| Actual window | Bar for a framework | Otherwise |
|---|
| < 90 days | ≥3 posts and spread across ≥3 distinct calendar months, or a theme the author explicitly names as a principle | Label it an observation, not a framework |
| ≥ 90 days | ≥3 posts, spread across ≥2 distinct months | Observation |
A short window cannot distinguish a conviction from a phase. On a five-week
corpus, near-daily repetition of one topic is evidence of what the author is
working on now, not of a durable framework — say so instead of promoting it.
Handle a collapsed window before generating
The collector warns when the returned window covers far less than requested and
records window_warning in manifest.json. When that warning is present:
- Tell the user before generating: state requested vs actual span and that
high-volume accounts exhaust
--limit long before the requested period.
- Offer the fix: re-collect with a higher
--limit, or narrow --since to
match what is actually reachable and accept the smaller scope honestly.
- If they proceed anyway, apply the short-window bar above, and put the
actual span in the generated skill's description — not just a footnote.
Never present a collapsed window as if it were the requested one.
Weigh quote-tweet evidence lower
A quote tweet proves the author endorsed someone else's framing. It does not
give you their words. When a theme rests mainly on quote tweets:
- attribute the framework to the endorsement, never to the quoted author;
- prefer a citation where the author states the idea in their own words;
- if every citation for a theme is a quote tweet, label it an observation.
Step 4 — Generate the skill
Write the skill directly from the Step 3 clustering report, following
book-to-skill's output contract (SKILL.md + chapters/ + supporting files).
Do not shell out to book-to-skill <corpus>.md. Verified 2026-08-26: its CLI
takes a document path and re-derives structure from the text — it has no flag
for supplying a prior analysis, so Mode 3 ("Generate from Prior Analysis") is a
workflow the agent performs, not a command the CLI exposes. Running it on the
corpus would discard the theme clustering that Step 3 exists to produce, and
re-derive chapters from raw chronology.
Keep source evidence outside the generated skill, in its collected
<user>-corpus/ directory. The skill points to that immutable snapshot; it does
not copy it.
- Record
source_corpus_path, manifest.json actual window, and the source
corpus collection timestamp in the generated skill's front matter or Sources
section.
- Record source URLs and the
chunks/YYYY-MM.md files read for every framework.
- Keep
analysis.md only in <user>-corpus/, including dropped themes and
contradictions. Do not copy it into the skill.
- If the source snapshot must travel with the skill, package the two directories
together unchanged; do not duplicate
raw.jsonl, chunks, analysis, or an
optional corpus.md inside the skill directory.
Two adjustments to book-to-skill's output contract:
- Chapters are themes, not dates. Chronological chapters would reproduce
the timeline, which is the thing being escaped.
- Every framework carries its post URLs. book-to-skill cites chapters; here
the citation is the permalink.
The generated skill's front-matter description must name the account and the
date window, e.g. "Working patterns from @karpathy's X posts (2024-06 –
2026-08)." This keeps the scope honest at load time.
Anti-patterns
Summarizing the timeline. Chronological recap is the failure mode. Nobody
needs "in March they posted about X." Structure by idea or don't ship.
Promoting one viral post to a framework. High engagement measures reach,
not conviction. The engagement counts in the corpus are context, not ranking.
Smoothing the contradictions. A corpus spanning years contains genuine
reversals. Presenting a single coherent position from contradictory posts is
fabrication by omission.
Filling gaps with the plausible. If the author never addressed something,
the skill says nothing about it. Do not complete their worldview for them.
Mistaking jokes for doctrine. Timelines are largely social. Irony, replies,
and throwaway lines are not teaching material.
Scope
Public posts only. Extracts ideas, not identity. Produces a citable knowledge
base from someone's published thinking — not a profile of a person, not a voice
clone, and not a claim to represent them.