# tweet-factory documentation > Concatenation of the markdown pages linked from https://docs.tweetfactory.sh/llms.txt --- Source: https://docs.tweetfactory.sh/index.md # Overview tweet-factory is a local [Bun](https://bun.sh) + TypeScript CLI, `tf`. It learns from one X account’s likes, bookmarks, and posts, stores them in SQLite, and drafts new tweets in that person’s voice with their own LLM. The project directory is the product. Local files are the default. Optional [cloud sync](cloud-sync.md) (`tf login`, then `tf sync push`) copies that directory to a tweetfactory.sh account. Nothing is uploaded until you push. ## What a run actually does 1. `tf auth` completes OAuth 2.0 (PKCE) and writes `.tokens.json`. 2. `tf fetch` pulls likes, bookmarks, and the account’s own posts into `tweets.db`. 3. A persona file, `persona/PERSONA.md`, records voice rules. `tf onboard` can write it, or you edit the template. 4. `tf generate` asks your LLM for four ideas, each with two or three variants. The prompt includes measured voice stats, your reply/quote/repost/like rates when `tweets.db` has enough impressions, and the published For You weights ([`xai-org/x-algorithm`](https://github.com/xai-org/x-algorithm) commit [`cb45b55`](https://github.com/xai-org/x-algorithm/commit/cb45b55)). `drafts/YYYY-MM-DD.md` shows a per-head breakdown and a 0–100 score, and tells you to post one variant. 5. `tf refresh` refreshes the database and writes what’s new to `flush/pending.md`. It does not call an LLM. `tf update` is separate: it replaces a release binary. You publish the drafts yourself. `tf` never posts to X. ## How the For You ranker is described here These pages quote [`X_ALGORITHM_BOOST_PLAYBOOK.md`](https://github.com/abhirupghosh/tweet-factory/blob/main/X_ALGORITHM_BOOST_PLAYBOOK.md), which was checked against [`xai-org/x-algorithm`](https://github.com/xai-org/x-algorithm) commit [`cb45b55`](https://github.com/xai-org/x-algorithm/commit/cb45b55) (2026-10-09). They do not add engagement statistics that file does not contain. - [How the X (Twitter) algorithm works](how-the-x-algorithm-works.md) - [X algorithm changes in 2026](x-algorithm-changes-2026.md) - [How to grow on X](x-algorithm-engagement-weights.md) - [Best time to post on X](best-time-to-post-on-x.md) - [Myths: links, hashtags, bookmarks, Premium](x-algorithm-myths.md) - [Shadowban on X: what the code filters](shadowban-on-x.md) - [Tweet reply generator](why-replies-stay-in-network.md) - [How to reset your X algorithm](how-to-reset-your-x-algorithm.md) - [How tweet-factory scores drafts](how-tweet-factory-scores-drafts.md) - [What is TweepCred](what-is-tweepcred.md) - [Twitter algorithm on GitHub](twitter-algorithm-github.md) - [Tweet generator in your voice](ai-tweet-writer-your-voice.md) - [Using tweet-factory with Claude Code](claude-code-twitter.md) - [Compared with Typefully, Tweet Hunter, and Hypefury](compare-typefully-tweet-hunter-hypefury.md) - [Typefully alternative](typefully-alternative.md) - [Tweet Hunter alternative](tweet-hunter-alternative.md) - [Hypefury alternative](hypefury-alternative.md) ## Start here - A person installing it: [Install](install.md), then [X app setup](x-app.md). - A coding agent installing it for someone: [copy-paste prompt](agent-prompt.md), then the [runbook](agent.md). - Machine-readable index: [/llms.txt](/llms.txt). Full text of every page: [/llms-full.txt](/llms-full.txt). Each HTML page also has a markdown twin (`/install.md`, and `/install/index.md`). ## Files that matter | Path | Role | |---|---| | `tweets.db` | Likes, bookmarks, your posts, article text, tagged Notion pages | | `persona/PERSONA.md` | Voice source of truth. Gitignored once filled in | | `flush/pending.md` | New interactions since the last flush commit | | `drafts/YYYY-MM-DD.md` | Scored drafts | | `onboarding.json` | Structured bundle from the last `tf generate` | | `.env` | API keys. Mode should be `0600` | | `.tokens.json` | OAuth tokens. Written mode `0600` | | `.cloud.json` | Optional cloud-sync device token. Written mode `0600` by `tf login`. Absent until you link | | `X_ALGORITHM_BOOST_PLAYBOOK.md` | Human-readable ranker write-up. Scoring uses `src/rank/weights.ts` | Install the release binary with the command on [Install](install.md): `curl -fsSL https://tweetfactory.sh/install.sh | sh`. The site build serves that script. It downloads `tf-darwin-arm64`, `tf-darwin-x64`, `tf-linux-x64`, or `tf-linux-arm64` from `https://tweetfactory.sh/download/vX.Y.Z/` and checks `SHA256SUMS`. The current tag is `https://tweetfactory.sh/download/latest/VERSION`. The repository is private. All rights reserved. --- Source: https://docs.tweetfactory.sh/agent-prompt.md # Copy-paste setup prompt Paste this to a coding agent in the environment where tweet-factory should run. The agent clones the repo, follows the runbook, and stops where a person has to click or paste a key. ```prompt Set up tweet-factory on this machine for me. Read these first, in order, and follow them: 1. https://docs.tweetfactory.sh/llms.txt 2. https://docs.tweetfactory.sh/agent.md 3. https://docs.tweetfactory.sh/install.md 4. https://docs.tweetfactory.sh/x-app.md agent.md is the same runbook as SETUP.md in https://github.com/abhirupghosh/tweet-factory (plus this prompt). When a step and the repo disagree, trust the repo source and say so. Rules: - Install tf with the command in https://docs.tweetfactory.sh/install.md. Do not clone the repo unless the user already has a checkout or asked to develop from source. - If a checkout is already on this machine, stay in it and use ./setup_x.sh. Do not delete it. - Never invent credentials, tokens, or API keys. Never print secret values back to me. - Write secrets only into .env. After writing, run chmod 600 .env. .tokens.json is created by tf auth and is already mode 0600. - Do every step you can do yourself. Stop and ask me when a step says "Human required". - After each step, run that step's verification check before continuing. - tweet-factory writes drafts to files. It does not post to X. Do not post tweets for me. - Run tf fetch on this machine only. The X refresh token rotates on every use; a second machine invalidates it. Steps that need me (a human), which you cannot finish yourself: - Creating an X developer app and copying the OAuth 2.0 Client ID and Client Secret. - Clicking "Authorize app" in a browser during tf auth. - Pasting any optional key I choose to provide (Tavily, Notion, Slack, or a direct LLM API key). Start by installing tf (see the install page) and running `tf --help`. If you are already inside a checkout, run ./setup_x.sh --check instead and tell me what is missing. ``` The checks behind that prompt are the [setup runbook](agent.md). It is generated from `SETUP.md` in the repo so the two stay the same text. --- Source: https://docs.tweetfactory.sh/agent.md # Set this up for me Give an agent the [copy-paste prompt](agent-prompt.md). The steps it must follow are below. They are `SETUP.md` from the tweet-factory repo, included verbatim at build time (headings shifted down one level so this page keeps a single title). > **Human required.** Three moments need a person: creating the X app, clicking Authorize in the browser, and pasting any API keys. The agent does the rest. ```prompt Set up tweet-factory on this machine for me. Read these first, in order, and follow them: 1. https://docs.tweetfactory.sh/llms.txt 2. https://docs.tweetfactory.sh/agent.md 3. https://docs.tweetfactory.sh/install.md 4. https://docs.tweetfactory.sh/x-app.md agent.md is the same runbook as SETUP.md in https://github.com/abhirupghosh/tweet-factory (plus this prompt). When a step and the repo disagree, trust the repo source and say so. Rules: - Install tf with the command in https://docs.tweetfactory.sh/install.md. Do not clone the repo unless the user already has a checkout or asked to develop from source. - If a checkout is already on this machine, stay in it and use ./setup_x.sh. Do not delete it. - Never invent credentials, tokens, or API keys. Never print secret values back to me. - Write secrets only into .env. After writing, run chmod 600 .env. .tokens.json is created by tf auth and is already mode 0600. - Do every step you can do yourself. Stop and ask me when a step says "Human required". - After each step, run that step's verification check before continuing. - tweet-factory writes drafts to files. It does not post to X. Do not post tweets for me. - Run tf fetch on this machine only. The X refresh token rotates on every use; a second machine invalidates it. Steps that need me (a human), which you cannot finish yourself: - Creating an X developer app and copying the OAuth 2.0 Client ID and Client Secret. - Clicking "Authorize app" in a browser during tf auth. - Pasting any optional key I choose to provide (Tavily, Notion, Slack, or a direct LLM API key). Start by installing tf (see the install page) and running `tf --help`. If you are already inside a checkout, run ./setup_x.sh --check instead and tell me what is missing. ``` ## Runbook > Included from [`SETUP.md`](https://github.com/abhirupghosh/tweet-factory/blob/main/SETUP.md) when this site is built. Edit that file, not a copy. ## Setting up tweet-factory for a new user — agent runbook **Audience: a coding agent (Claude Code, Cursor, etc.) setting this up for a user on their machine.** Follow these steps in order. Ask the user whenever you need a key, secret, or decision — never guess or invent credentials. Keep the user in the loop; the mechanical work is a few commands plus one browser click from them. > The mechanical work is `install.sh` for a new machine, or `./setup_x.sh` when you are already inside a checkout (Bun/TypeScript — no Python). Your job is to > collect the inputs, run the install that matches the machine, help the user fill their persona, and hand them the loop prompt. ### Who does what | Step | Who | |---|---| | Install `tf` (script, or `./setup_x.sh` when already in a checkout), write `.env`, `chmod 600`, fetch, persona file, enrich | Agent | | Create the X developer app and copy the OAuth 2.0 Client ID and Client Secret | **Human** | | Click **Authorize app** in the browser during `tf auth` | **Human** | | Paste optional keys (Tavily, Notion, Slack, direct LLM API) if they want them | **Human** | A copy-paste prompt that points an agent at this runbook lives at [docs.tweetfactory.sh/agent-prompt.md](https://docs.tweetfactory.sh/agent-prompt.md). The GitHub repository is private. Release binaries are still public at `https://tweetfactory.sh/download/`. Do not treat a binary as a substitute for keeping the source private. --- ### Step 0 — Install `tf` Prefer the release binary. It does not clone the repo: ```bash curl -fsSL https://tweetfactory.sh/install.sh | sh mkdir -p ~/tweet-factory && cd ~/tweet-factory tf --help ``` The site build serves `install.sh`. The script reads `https://tweetfactory.sh/download/latest/VERSION`, then downloads `tf` and `SHA256SUMS` for that tag from `https://tweetfactory.sh/download/vX.Y.Z/`. `TF_VERSION=v0.3.0` pins a tag. > **Verify.** `tf --help` exits 0 and prints `tweet-factory (tf)`. `command -v tf` is on `PATH`. If this machine **already has a checkout** (a directory with `setup_x.sh` and `package.json`), stay there. Do not reinstall over it and do not delete it. Run a read-only check first — it reports missing tools and keys without changing anything: ```bash ./setup_x.sh --check ``` > **Verify.** The command exits 0 and prints a tool/key report. It must not create or edit `.env`. A binary install has no `setup_x.sh`. Skip the check and continue with Step 1 in the data directory. `tf auth` writes `.env.example` there when it is missing. ### Step 1 — Get the user's X API credentials (REQUIRED) The only hard requirement is an X app with **OAuth 2.0** (the sole auth X allows for reading bookmarks). Walk the user through it, then collect the keys by asking them. Do not invent values. > **Human required.** The user does this in a browser at https://developer.x.com/en/portal/dashboard → their app → **User authentication settings**: > > - App permissions: **Read** > - Type of App: **Web App, Automated App or Bot** (confidential client → gives a refresh token) > - Callback URI: `http://127.0.0.1:8675/callback` (must be exact) > - Save, then open the **Keys and tokens** tab and copy the **OAuth 2.0 Client ID and Client Secret**. `tf auth` requests these scopes: `tweet.read`, `users.read`, `like.read`, `bookmark.read`, `offline.access`. Ask the user for: - `TWITTER_OAUTH2_CLIENT_ID` (required) - `TWITTER_OAUTH2_CLIENT_SECRET` (required) Write them into `.env` (copy `.env.example` first if `.env` doesn't exist). On a binary install, `tf auth` writes `.env.example` when it is missing. Never echo secret values back. Then lock the file down: ```bash chmod 600 .env ``` > **Verify.** Both keys are non-empty, and the mode is `600`. This prints only `set` / `missing`, never the values: > > ```bash > stat -c '%a' .env # macOS: stat -f '%OLp' .env > awk -F= '$1=="TWITTER_OAUTH2_CLIENT_ID"{id=$2} $1=="TWITTER_OAUTH2_CLIENT_SECRET"{sec=$2} END{if(length(id)&&length(sec)) print "oauth keys: set"; else {print "oauth keys: missing"; exit 1}}' .env > ``` ### Step 2 — Optional integrations (ask, offer to skip) Ask, with a clear skip option. All of these can stay blank: - `TF_LLM_PROVIDER` + a key — only if the user wants direct-API generation instead of the default Claude Code CLI backend. Default `tf generate` spawns `claude -p` and needs no key. Providers: `anthropic` (`ANTHROPIC_API_KEY`), `openrouter` (`OPENROUTER_API_KEY`), `openai` (`OPENAI_API_KEY`). Optional `TF_LLM_MODEL` overrides the model. - `TAVILY_API_KEY` — free at https://tavily.com. Enables `tf search`, `tf enrich`, and `tf generate --timely`. Recommended. - `NOTION_API_KEY` — internal integration at https://www.notion.so/my-integrations. The user must share pages with the integration before `tf notion` can see them. - `SLACK_WEBHOOK_URL` — https://api.slack.com/messaging/webhooks. If set, drafts can be pushed to Slack. > **Human required.** Pasting any of these keys. Skipping is a complete setup. > > **Verify.** `.env` is still mode `600`. Do not print the values. ### Step 3 — Authorize and back-fill Once `.env` has at least the two OAuth 2.0 values: **Checkout** ( `./setup_x.sh` is in this directory ): ```bash NONINTERACTIVE=1 ./setup_x.sh ``` A human at the keyboard can run `./setup_x.sh` instead and let it ask for anything still blank. It installs Bun, builds `tf`, then runs the OAuth flow and the first fetch. **Binary install** (no `setup_x.sh`): ```bash tf auth tf fetch tf flush || true ``` > **Human required.** A browser opens (or a URL is printed). The user logs in as the account they want to analyze and clicks **Authorize app**. On a headless machine, pass `--no-browser` via `tf auth --no-browser` and have them open the printed URL. If the callback must be reachable from another machine, set `OAUTH_REDIRECT_URI` and `OAUTH_BIND_HOST` and register that exact callback in the X app. The default bind is `127.0.0.1`. The first flush marks everything already in the database as processed, so `flush/pending.md` starts at 0 new items. Later runs of `tf refresh` show only what's new. > **Verify.** > > ```bash > if [ -x ./tf ]; then TF=./tf; else TF=tf; fi > test -s .tokens.json && echo "tokens: present" > test -s tweets.db && echo "db: present" > "$TF" analyze > ``` > > `tf analyze` should print a handle and non-zero counts for the sources the account actually has. `Has refresh token: true` should have appeared in the auth output (the app needs a confidential client). ### Step 4 — Fill the persona `setup_x.sh` does **not** write `persona/PERSONA.md`. A binary install writes `persona/PERSONA.template.md` on `tf auth` or `tf onboard` when the file is missing. Create the persona one of these ways: - Copy the template and edit it: `cp persona/PERSONA.template.md persona/PERSONA.md` - Or run `tf onboard` and press `f` on the forge screen. That writes `persona/PERSONA.md` from the four interview answers (what they want to be known for, a hot take, what they're building, three voice words), the account tags, and measured stats. Make it real — this is what makes drafts sound like the user: - Ask: their handle, the lane they want to be known for, voice rules (casing, length, tone), and any banned phrases. - Or infer from data: `tf analyze` summarizes measured voice signals (lowercase-start rate, median length, top liked authors). Confirm the draft with the user before treating it as final. `tf generate` still runs with no persona file: it infers from those measured signals. A filled-in `persona/PERSONA.md` is the voice source of truth. > **Verify.** `persona/PERSONA.md` exists, and it is not still the untouched template (`@YOUR_HANDLE` should be gone). `./tf analyze` exits 0. ### Step 5 — (Optional) enrich article bookmarks If `TAVILY_API_KEY` is set and you want full article text behind their bookmarks: ```bash ./tf enrich ``` Without that key the command errors. Skip the step when the key is absent. > **Verify.** The command exits 0, or you skipped it because `TAVILY_API_KEY` is unset. ### Step 6 — Hand off the loop prompt Tell the user how to generate drafts from here. For a one-off batch: `./tf generate`. In Claude Code, `/tweet-factory` runs the bundled skill, which calls `tf` for data and then drafts. For a repeating Claude Code loop, pick based on Slack: **With Slack:** ``` /loop 6h Spawn a subagent via the Agent tool with model "opus" to run /tweet-factory end-to-end, then post that run's drafts to Slack via ./tf notify drafts/$(date +%F).md "🧵 fresh tweet ideas". Reply with only a one-line summary. Keep going on this interval. ``` **Terminal only (no Slack):** ``` /loop 6h Spawn a subagent via the Agent tool with model "opus" to run /tweet-factory end-to-end and show me the drafts. Keep going on this interval. ``` > **Verify.** The user has a command they can run (`./tf generate`) and, if they wanted Slack, `SLACK_WEBHOOK_URL` is set. Do not run an infinite loop yourself unless they asked for one. --- ### Reminders - **Single-host rule:** `tf fetch` (and any cron loop) must run on ONE machine only — the X refresh token rotates on every use; running it on two hosts invalidates the token. - **Secrets never get committed:** `.env`, `.tokens.json`, `tweets.db`, `persona/PERSONA.md`, `flush/*`, `drafts/*`, and `onboarding.json` are gitignored. - **Re-auth:** if a run logs a 401, run `./tf auth` once to mint a new token. - **Scheduling:** `tf refresh` is the non-LLM refresh (safe to cron). `tf cron` also runs enrich, so it needs `TAVILY_API_KEY` or it errors after the fetch. `tf update` replaces a release binary and is not the database refresh. See [deploy](https://docs.tweetfactory.sh/deploy/) and the Deploy section in [`README.md`](https://github.com/abhirupghosh/tweet-factory/blob/main/README.md). Scheduling on a server is the [deploy and cron](deploy.md) page. --- Source: https://docs.tweetfactory.sh/install.md # Install Generation uses the Claude Code CLI by default, or an API key if you set one. The X app is covered in [X app setup](x-app.md). The GitHub repository is private. The release binary is the install path for users. Source stays in the private repo. ## Fast path ```bash curl -fsSL https://tweetfactory.sh/install.sh | sh mkdir -p ~/tweet-factory && cd ~/tweet-factory tf auth ``` `bun run sites:build` copies `install.sh` to the landing site as `/install.sh`. The script reads [the current tag](https://tweetfactory.sh/download/latest/VERSION), then downloads `tf-darwin-arm64`, `tf-darwin-x64`, `tf-linux-x64`, or `tf-linux-arm64` and `SHA256SUMS` from `https://tweetfactory.sh/download/vX.Y.Z/`. Those URLs redirect to a public Blob store. GitHub Release assets are not the download. Set `TF_INSTALL_DIR` to put `tf` somewhere else. Set `TF_VERSION=v0.3.0` to pin a tag. `tf auth` writes `.env.example` when that file is not already in the directory. Copy it to `.env`, fill in the two X OAuth keys, `chmod 600 .env`, and run `tf auth` again. Then `tf fetch`. Later, from that binary: ```bash tf update --check tf update ``` `tf update` compares `tf version` with the tag in `latest/VERSION`. When a newer tag is published, it downloads the matching binary, checks `SHA256SUMS`, and replaces the executable. `--check` only prints the comparison. A checkout run with `bun run src/index.ts update` refuses; pull the repo, or install the binary above. > **Verify.** `tf --help` prints `tweet-factory (tf)` and exits 0. `command -v tf` prints the install path. ## Release assets Built on native runners (OpenTUI’s native addon is not cross-compiled): | Asset | Machine | |---|---| | `tf-darwin-arm64` | macOS Apple silicon | | `tf-darwin-x64` | macOS Intel | | `tf-linux-x64` | Linux x64 | | `tf-linux-arm64` | Linux arm64 | | `SHA256SUMS` | checksums for the four binaries | Windows builds are not published. Check a manual download with `sha256sum -c SHA256SUMS` (macOS: `shasum -a 256 -c SHA256SUMS`). The binary looks upward from the working directory for `tweets.db`, `.env`, or `package.json`, then falls back to the current directory. Pass `--dir` to point at a specific root. Scoring is compiled into the binary. You do not need `X_ALGORITHM_BOOST_PLAYBOOK.md` on disk for `tf generate` to score drafts. > **Verify.** `tf --help` lists `auth`, `fetch`, `onboard`, `generate`, `notion`, `enrich`, `flush`, `search`, `notify`, `refresh`, `update`, `cron`, and `analyze`. ## Checkout you already have `./setup_x.sh` still installs Bun, runs `bun install`, compiles `./tf`, runs `tf auth`, then `tf fetch` and a baseline `tf flush`. Leave a working checkout in place. The installer is for machines that do not have the repo. ```bash ./setup_x.sh ``` Agents that already wrote `.env` should run `NONINTERACTIVE=1 ./setup_x.sh` so the script does not block on prompts. The browser authorize step still needs a person. A read-only preview that changes nothing: ```bash ./setup_x.sh --check ``` Contributors run the TypeScript directly: ```bash bun run src/index.ts --help ``` > **Verify.** `./tf --help` prints the command list. `./setup_x.sh --check` exits 0. --- Source: https://docs.tweetfactory.sh/x-app.md # X app setup > **Human required.** A person creates this app in a browser. An agent cannot click through the X developer portal for you. Bookmarks are readable only with OAuth 2.0 user context. An app-only bearer token gets a 403 on that endpoint, which is why tweet-factory dropped the old OAuth 1.0a path. ## In the developer portal 1. Open [developer.x.com](https://developer.x.com/en/portal/dashboard) and create an app (the free tier is enough for read access). 2. Under **User authentication settings** set: - **App permissions:** Read - **Type of App:** Web App, Automated App or Bot (this is a confidential client, which is what returns a refresh token) - **Callback URI:** `http://127.0.0.1:8675/callback` - Website URL: any URL you control. The CLI does not call it. 3. Save. 4. Open **Keys and tokens** and copy the **OAuth 2.0 Client ID** and **Client Secret** into `.env` as `TWITTER_OAUTH2_CLIENT_ID` and `TWITTER_OAUTH2_CLIENT_SECRET`. The callback must match exactly, including the port `8675` and the path `/callback`. `tf auth` binds `127.0.0.1` unless you change it. ## Scopes the CLI requests `tweet.read`, `users.read`, `like.read`, `bookmark.read`, `offline.access`. `offline.access` is the refresh token. Without it, the access token expires and every later fetch needs a new browser login. A public client (native app) often will not return a refresh token; use the confidential web-app type and keep the client secret. ## Authorize ```bash ./tf auth ``` A browser opens. Log in as the account whose likes and bookmarks you want, and click **Authorize app**. The CLI writes `.tokens.json` (mode `0600`) and prints the granted scopes plus whether a refresh token is present. Headless: ```bash ./tf auth --no-browser ``` Open the printed URL on a machine with a browser. The callback still has to hit the process that is waiting. From another machine, set `OAUTH_BIND_HOST` and `OAUTH_REDIRECT_URI` to a reachable URL, register that exact URI on the app, and pass the same values (`--bind`, `--redirect-uri`). The default wait is 5 minutes (`--timeout` seconds). > **Verify.** `.tokens.json` exists, is non-empty, and the auth log said `Has refresh token: true`. File mode is `600` (`stat -c '%a' .tokens.json`). ## One machine The refresh token rotates every time it is used. Run `tf fetch` / `tf refresh` / `tf cron` on a single machine. A second checkout that refreshes the same token invalidates the first, and the next call returns 401. Fix that with one `tf auth` on the machine you intend to keep. > **Verify.** After auth, `./tf fetch` prints a database summary with `like`, `bookmark`, and `own` counts (a brand-new account may legitimately have zeros). --- Source: https://docs.tweetfactory.sh/configuration.md # Configuration Copy `.env.example` to `.env`. The parser is lenient: blank lines and `#` comments are ignored, and the value is everything after the first `=`. `tf` loads `.env` from the project root (the nearest directory containing `tweets.db`, `.env`, or `package.json`, or the path passed to `--dir`). After you write secrets: ```bash chmod 600 .env ``` `tf` and `setup_x.sh` set mode `0600` when they write `.env`. A hand-edited file does not. Run `chmod 600 .env` after you edit it yourself. Never commit `.env`. Never paste values into chat. The table below is generated from the keys in `.env.example`. Adding a key there without documenting it fails the docs build. | Variable | Needed | What it does | |---|---|---| | `TWITTER_OAUTH2_CLIENT_ID` | Required | OAuth 2.0 client id from the X app’s Keys and tokens tab. | | `TWITTER_OAUTH2_CLIENT_SECRET` | Required | OAuth 2.0 client secret. Confidential web app so the token response includes a refresh token. | | `TF_LLM_PROVIDER` | Optional | `claude-code` (default), `anthropic`, `openrouter`, or `openai`. | | `TF_LLM_MODEL` | Optional | Overrides the provider default. For Claude Code this is passed as `claude --model`. | | `ANTHROPIC_API_KEY` | With provider anthropic | Direct Anthropic API. Unused when the backend is Claude Code. | | `OPENROUTER_API_KEY` | With provider openrouter | OpenRouter key. Requests go to `https://openrouter.ai/api/v1`. | | `OPENAI_API_KEY` | With provider openai | OpenAI key for `tf generate` when the provider is `openai`. | | `TAVILY_API_KEY` | Optional | Timely search, article extract, `tf generate --timely`, and `tf cron` (cron calls enrich). | | `NOTION_API_KEY` | Optional | Internal integration secret. Pages must be shared with the integration. | | `SLACK_WEBHOOK_URL` | Optional | Incoming webhook for `tf notify`, `--post-to-slack`, and the cron pending ping. | | `TF_LICENSE_KEY` | Optional | License key for paid features. The default build allows every feature and does not send the key. | | `TF_API_ORIGIN` | Optional | Entitlement API origin once a billing provider is linked. Default `https://tweetfactory.sh`. | | `TF_CLOUD_ORIGIN` | Optional | Cloud sync API origin. Default `https://tweetfactory.sh`. Nothing is uploaded until `tf login` and `tf sync push`. | | `OAUTH_REDIRECT_URI` | Optional | Defaults to `http://127.0.0.1:8675/callback`. Must be registered on the X app. | | `OAUTH_PORT` | Optional | Callback listen port. Default `8675`. | | `OAUTH_BIND_HOST` | Optional | Callback bind address. Default `127.0.0.1`. Widen only for cross-machine auth. | ## Project root Global flag on every command: ```bash tf --dir /path/to/checkout fetch ``` Without `--dir`, `tf` walks up to six directories from the working directory looking for `tweets.db`, `.env`, or `package.json`. ## What stays out of git `.env`, `.tokens.json`, `.cloud.json`, `tweets.db`, `persona/PERSONA.md`, `flush/*` (except `.gitkeep`), `drafts/*` (except `.gitkeep`), `onboarding.json`, `logs/`, `backups/`. `.cloud.json` appears only after `tf login`. > **Verify.** `git check-ignore -v .env .tokens.json .cloud.json tweets.db persona/PERSONA.md` prints a matching ignore rule for each path that exists. `stat` shows mode `600` on `.env` and `.tokens.json`. After `tf login`, `.cloud.json` is mode `600` too. --- Source: https://docs.tweetfactory.sh/commands.md # Commands ``` tf [options] tweet-factory # same program, via the package bin ``` `tf --help`, `tf -h`, and `tf help` print the short list and exit 0. `tf --version`, `tf -v`, and `tf version` print `tf ` and exit 0. An unknown command prints the help and exits 2. Every command accepts `--dir ` (see [Configuration](configuration.md)). This page is checked against the command table in `src/cli.ts` when the docs build: a new command that is missing here fails the build. ## tf auth OAuth 2.0 PKCE. Writes `.tokens.json`. ```bash tf auth [--port N] [--bind HOST] [--redirect-uri URI] [--no-browser] [--timeout SECS] ``` | Flag | Default | |---|---| | `--port` | `OAUTH_PORT` or `8675` | | `--bind` | `OAUTH_BIND_HOST` or `127.0.0.1` | | `--redirect-uri` | `OAUTH_REDIRECT_URI` or `http://127.0.0.1:/callback` | | `--no-browser` | off; when set, the URL is printed and no browser is launched | | `--timeout` | `300` seconds | Scopes: `tweet.read`, `users.read`, `like.read`, `bookmark.read`, `offline.access`. Details and the human click are in [X app setup](x-app.md). > **Human required.** Someone clicks **Authorize app** in a browser. > > **Verify.** `.tokens.json` exists and the log includes `Has refresh token: true`. ## tf fetch Pull likes, bookmarks, and your posts into `tweets.db`. ```bash tf fetch [like|bookmark|own] [--incremental] ``` With no source words, it fetches all three. User id comes from `/2/users/me`. Pages are 100 tweets, with a one-second pause between pages. A 401 refreshes the access token and retries. A 403 skips that source. A 429 sleeps until the rate-limit reset. `--incremental` stops a source early once a page contains no ids that are already stored. The first pull should be a full fetch (no flag). `tf refresh` and `tf cron` always fetch incrementally. > **Verify.** The log ends with a database summary: counts for `like`, `bookmark`, `own`, and distinct authors. `test -s tweets.db`. ## tf onboard OpenTUI onboarding. Needs a terminal. ```bash tf onboard [--selftest] ``` Screens, in order: welcome, four interview questions, account tagging, a heuristic roast, forge, optional Notion, generate. Interview questions: 1. What do you want to be known for, in one breath? 2. Your hottest take that's gotten you in trouble? 3. What are you building right now? 4. Describe your voice in 3 words. Account tags, cycled with space: (none), `inspiration`, `competitor`, `north-star`, `friend`. Arrow keys move. Enter continues. The roast uses the same per-head scorer as `tf generate`: editorial strengths times the published action weights. It does not apply account calibration. `tf analyze` prints whether your own history is thick enough to calibrate. On the forge screen, `f` writes `persona/PERSONA.md`. `n` opens Notion search. `g` generates drafts with your LLM. `q` quits. `--selftest` renders one frame and exits. It still starts the TUI renderer, so it needs an environment where `tf onboard` can open. > **Verify.** After forge, `persona/PERSONA.md` exists and names the handle `tf analyze` prints. ## tf generate Draft tweets with your LLM. Writes `drafts/YYYY-MM-DD.md` and `onboarding.json`. ```bash tf generate [--n N] [--timely] [--post-to-slack] [--provider NAME] [--model NAME] [--json] [--no-style-check] ``` | Flag | Effect | |---|---| | `--n` | How many ideas to ask for. Default `4`. Each idea is 2–3 variants. | | `--timely` | One Tavily news search is added to the prompt when `TAVILY_API_KEY` is set. The query is `AI agents coding startups` when your posts hit the analyzer’s energy-marker list, otherwise `technology`. For a query you choose, run `tf search` yourself. | | `--post-to-slack` | After writing the file, post it with `tf notify`. | | `--provider` | Overrides `TF_LLM_PROVIDER` for this run. | | `--model` | Overrides `TF_LLM_MODEL` for this run. | | `--json` | Print the bundle on stdout instead of writing the markdown file. `onboarding.json` is still written. | | `--no-style-check` | Skip the [style check](#style-check). The prompt's style rules are dropped too. | The model is asked for one JSON object: persona summary, content pillars, reply targets, cadence, strategy, `ideas` (each with 2–3 variants), and a flat `drafts` array. The model’s `score` field is ignored. After Zod validates the JSON, tweet-factory scores every variant locally: copy-link share, reply (20 on an original a mutual would see, 5 otherwise), DM share, quote, follow, repost, like, and the negative heads report, mute, not-interested, and block. The markdown file shows that breakdown, the weighted total, and a 0–100 score. A note at the top says to post one variant per idea, because a second post from you in one viewer’s pool is multiplied by 0.625 and DPP separates near-duplicates. When `tweets.db` has at least 8 of your own posts with impressions, reply, quote, repost, and like rates (per impression, per format) scale those four strengths. Bookmarks are printed as a diagnostic and do not scale a head. Fewer than 8 impressed posts leaves the scales at 1 and says the history is thin. Posts older than 48 hours are preferred when 8 of them exist. Context sent as fenced reference data (not as instructions): `flush/pending.md` when it has new items, a few recent posts, tagged Notion pages (about 6,000 characters), the calibration block, and the optional Tavily blurb. The system prompt is your persona plus measured stats plus the ranking prompt built from `src/rank/weights.ts`. Default backend and keys: [LLM providers](llm.md). Every variant goes through the [style check](#style-check) before it is scored, so the score in the file is the score of the text in the file. A variant the check touched shows a `Style check:` block with each fix and any flags left, and the score before fixes. In `--json` and `onboarding.json`, each variant carries `style: { fixes, flags, original?, originalScore? }`. stderr ends with a one-line count. > **Verify.** `drafts/.md` contains a `| Head |` table and `Post one variant`, and `onboarding.json` parses as JSON with a `drafts` array. `--json` prints that object and exits 0. ## tf brain-dump Turn raw dictation into an outline with notes and 2–3 scored angles, before full drafting. Same LLM, persona, and scorer as `tf generate`. ```bash tf brain-dump "rough thoughts, pasted or dictated" tf brain-dump --file memo.txt pbpaste | tf brain-dump - tf brain-dump --file memo.txt --draft 2 tf brain-dump [ | --file PATH | -] [--angles 2|3] [--draft N] [--json] [--provider NAME] [--model NAME] [--no-style-check] ``` | Flag | Effect | |---|---| | `` | Dictation as a quoted argument. | | `--file`, `-f` | Read the dictation from a file, such as a voice-memo transcript. | | `-` | Read stdin. Piped input is read without the dash. | | `--angles` | How many distinct angles to ask for, clamped to 2–3. Default `3`. | | `--draft N` | After printing, feed angle `N` into the `tf generate` flow. That writes `drafts/YYYY-MM-DD.md` and `onboarding.json` like `tf generate`. | | `--json` | Print the outline and scored angles as JSON. | | `--provider`, `--model` | Override `TF_LLM_PROVIDER` and `TF_LLM_MODEL` for this run. | | `--no-style-check` | Skip the [style check](#style-check) on the angles and on `--draft` drafts. | Output is a one-line summary, a numbered outline where each point can carry notes (your own words worth keeping, gaps), and the angles. Each angle is one tweet in your voice with its aim and a 0–100 score from the same per-head scorer as `tf generate`; the model's own scores are ignored. Angles go through the [style check](#style-check) first, and any fixes or flags print under the angle. Nothing is written to disk unless you pass `--draft`. No input exits 2. The dictation is sent to your LLM as fenced reference data, not instructions, and is cut at 20,000 characters. Persona loading and backends are the same as `tf generate` ([LLM providers](llm.md)). > **Verify.** `tf brain-dump --help` prints usage. With a configured LLM, `tf brain-dump "a rough thought"` prints `## Outline` and `### Angle 1`. ## tf lint Run the style check on one draft without an LLM. Fixed text goes to stdout and the report to stderr, so it pipes. ```bash tf lint "Here's the thing — agents need memory" pbpaste | tf lint - tf lint --file draft.txt --json --score tf lint [ | --file PATH | -] [--json] [--no-fix] [--strict] [--score] ``` | Flag | Effect | |---|---| | ``, `--file`, `-` | The draft: a quoted argument, a file, or stdin. | | `--no-fix` | Leave the text alone and report every rewrite as a flag. | | `--strict` | Exit 1 when a flag remains. | | `--score` | Add the 0–100 score before and after fixes (uncalibrated). | | `--json` | Print `{ text, original, changed, fixes, flags }` (plus `score`, `originalScore` with `--score`). | ```text $ tf lint "Here's the thing — agents need memory. Fast. Cheap. Reliable." Agents need memory. Fast. Cheap. Reliable. fixed (heres-the-thing): "Here's the thing — agents need" -> "Agents need" flag (triad): stacked rhetorical triad reads as AI; keep the one that matters: "Fast. Cheap. Reliable." ``` No input exits 2. ### Style check Inspired by Theo's `noEmDashCopy` lint rule. It runs on every draft `tf generate` and `tf brain-dump` produce (and on the generate screen in `tf onboard`), before the draft is scored or shown. The same rules are listed in the generation prompt so the model avoids them in the first place. The tweetfactory.sh score page runs the same check and offers the rewrite. Rewritten, with deterministic rules: | Pattern | Rewrite | |---|---| | `X — and/but/so/which …` | comma | | `X — it's / we / the / 3 …` (a new clause) | period, capitalized unless the draft is all lowercase | | `X — short tail.` (4 words or fewer) | colon | | `X — aside — Y` | parentheses | | dash before a line break / at the end / as a bullet | colon / `...` / `- ` | | anything else with an em dash, a spaced en dash, or ` -- ` | comma | | `pre–seed` (en dash between letters) | hyphen | | `delve into` (any tense) | `dig into` | | leading `Here's the thing` / `In today's fast-paced world,` | dropped | Kept as is: number ranges such as `2023–2026` and `2–3`, inline code, fenced code, and URLs. Flagged, not rewritten: `delve`, `tapestry`, `testament to`, `game-changer`, `unleash` / `unlock the potential`, `it's not X, it's Y` and `X isn't Y, it's Z`, `not just X, but Y`, leading `In a world where`, `let's dive in`, `ever-evolving landscape`, three or more one- or two-word sentences in a row or `no X, no Y, no Z`, more than 2 emoji, more than 1 hashtag. The list is one data file, `src/style/tells.ts`. Each entry is a regex, an optional replacement, a message, and the phrase the prompt names. Add a replacement only when it is safe in every sentence the pattern can match. > **Verify.** `tf lint "we shipped — and nobody cared"` prints `we shipped, and nobody cared` and exits 0. `tf lint --strict "a game-changer"` exits 1. ## tf notion Tag Notion pages as grounding for generation. Requires `NOTION_API_KEY`, and the pages must be shared with the integration. ```bash tf notion search [query] [--json] tf notion tag --tag