https://github.com/ivan-magda/blog-amplifier
CLI that finds existing X and LinkedIn conversations about a blog post or repo, ranks them, drafts comments, and routes them through a human-review gate. Posting stays manual.
https://github.com/ivan-magda/blog-amplifier
apify blogging claude cli content-marketing developer-tools linkedin nodejs social-media twitter typescript web-scraping
Last synced: 2 days ago
JSON representation
CLI that finds existing X and LinkedIn conversations about a blog post or repo, ranks them, drafts comments, and routes them through a human-review gate. Posting stays manual.
- Host: GitHub
- URL: https://github.com/ivan-magda/blog-amplifier
- Owner: ivan-magda
- License: mit
- Created: 2026-06-13T21:04:59.000Z (6 days ago)
- Default Branch: main
- Last Pushed: 2026-06-13T21:21:50.000Z (6 days ago)
- Last Synced: 2026-06-13T23:10:53.131Z (6 days ago)
- Topics: apify, blogging, claude, cli, content-marketing, developer-tools, linkedin, nodejs, social-media, twitter, typescript, web-scraping
- Language: TypeScript
- Size: 145 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# blog-amplifier
Find existing X/Twitter and LinkedIn conversations about your blog post or GitHub repo, rank them, draft a comment for each, and review the queue before you post by hand.
## Why it works this way
Scrapers read public posts without a login. Automated posting needs your account cookies and breaks platform rules. This tool automates the safe half (find, rank, draft) and leaves posting to you:
- **Read-only discovery.** Apify actors (xquik for X, HarvestAPI for LinkedIn) fetch public posts with no login and no cookies. Your accounts stay out of it.
- **A human gate.** The judge writes each candidate to `data/review-queue.csv`. Nothing moves forward until you set the `decision` column yourself.
- **Manual posting.** `record` prints a checklist. You post the approved comments at your own pace.
- **An idempotent ledger.** `data/actions.log.jsonl` records what you posted. Re-running `record` never double-records, and the next `discover` skips URLs you already engaged.
## Requirements
- Node 20.12 or newer. The CLI loads `.env` with `process.loadEnvFile`, so there is no `dotenv` dependency.
- An Apify token, from https://console.apify.com/account/integrations.
- The `claude` CLI, installed and logged in. The judge runs `claude -p` against your Claude Code subscription, so no metered API key is involved.
## Setup
```sh
npm install
cp .env.example .env # then set APIFY_TOKEN
claude # then run /login once, to authenticate the judge
```
Two optional judge overrides go in `.env`: `JUDGE_MODEL` (default `sonnet`) and `JUDGE_TIMEOUT_MS` (default `180000`).
## Quick start
Promote a GitHub repo across both platforms:
```sh
npm run add-subject -- --repo acme/widget-cli
npm run subjects # confirm the subject id
npm run pipeline -- --subject widget-cli # discover + judge
# open data/review-queue.csv and set `decision` to approve or reject
npm run record # prints what to post
```
Promote a blog post on X only:
```sh
npm run add-subject -- --blog ../blog/posts/rate-limiting-explained.md
npm run pipeline -- --subject rate-limiting-explained --platform x
# review data/review-queue.csv, then:
npm run record
```
Put flags after `--` when you run an npm script. To call the CLI directly, drop the `--`: `tsx src/cli.ts pipeline --subject widget-cli`.
## Commands
| Command | What it does |
|---|---|
| `add-subject --blog ` or `--repo ` | Read metadata and write `subjects/.json`. |
| `subjects` | List saved subject ids. |
| `discover --subject [--platform x\|linkedin\|both]` | Run the actors, drop URLs you already actioned, and write `data/candidates/-.candidates.json`. Default platform is both. |
| `judge --run ` | Score relevance, draft comments, write `data/queue/-.scored.json`, and append rows to `data/review-queue.csv`. |
| `record` | Read the approved rows, append them to the ledger, and print a manual posting checklist. |
| `pipeline --subject [--platform ...]` | Run `discover` then `judge` in one step. |
For usage text, run `tsx src/cli.ts help` (there is no `npm run help` script).
Which calls cost money: `discover` and `pipeline` spend Apify credits. `judge` uses your `claude` subscription and touches no paid API, so re-running it against saved candidates costs nothing. `add-subject`, `subjects`, and `record` make no paid calls.
## The review gate
`judge` writes one row per candidate to `data/review-queue.csv`. Open it in a spreadsheet or editor and set two columns:
- `decision`: `approve`, `reject`, or blank for pending.
- `final_comment`: optional. Leave it blank to use the AI `draft_comment`.
`record` then reads the `approve` rows, takes `final_comment` or falls back to `draft_comment`, appends each to the ledger, and prints the comments for you to post by hand.
## How ranking works
```
score = 0.6 · (relevance / 100) + 0.25 · engagement + 0.15 · recency
```
Relevance is the judge's 0-to-100 verdict. Engagement is a batch-normalized `log1p(likes + 2·replies + reposts)`. Recency decays on a 7-day half-life. Rows below `minScore` (0.45) drop out, and the judge keeps the top 15. The weights live in `src/config.ts`.
## Tuning a subject
`add-subject` writes a starting query, but you will get better results by editing `subjects/.json`. Each subject holds a per-platform query under `queries`:
```json
"queries": {
"x": "(\"rate limiting\" OR \"token bucket\") lang:en",
"linkedin": "rate limiting token bucket api"
}
```
Write multi-word topics as quoted phrases. On X an unquoted hyphen means exclusion, so `rate-limiting` searches for `rate` and *not* `limiting`. Keep `lang:en` on the X query. After editing, re-run `discover`; there is no need to re-run `add-subject`. A subject with zero keywords is rejected, because an empty query would scan every recent post and burn credits.
## Data layout
```
subjects/.json the subject you edit (keywords, per-platform queries)
data/candidates/-.candidates.json normalized candidates from one discover run
data/queue/-.scored.json ranked candidates with drafts
data/review-queue.csv the human gate you edit
data/actions.log.jsonl append-only ledger for idempotency and dedup
```
## Apify on the free plan
The defaults run on Apify's free plan:
- X uses `xquik/x-tweet-scraper`, about $0.15 per 1,000 results. On a paid plan you can swap it for `apidojo/tweet-scraper` in `src/config.ts`, which returns the same output fields. That actor blocks free-plan API access, so leave the default in place while you are on free.
- LinkedIn uses `harvestapi/linkedin-post-search`, from about $1.50 per 1,000 results, with no cookies or account.
Both search a window of roughly one week. Store prices drift, so check the live rate before a large run and try a scoped `--platform x` run first to gauge credit use.
## Troubleshooting
| Symptom | Cause and fix |
|---|---|
| `judge: WARNING — scored 0 of N candidates` | Every `claude` batch failed, so nothing was scored (not the same as finding no relevant posts). Confirm `claude` is logged in, raise `JUDGE_TIMEOUT_MS` if the error mentions a timeout, then re-run `judge --run latest`, which spends no Apify credits. |
| `APIFY_TOKEN is not set` | Set it in `.env`. |
| `Failed to spawn claude` | Install the `claude` CLI and run `/login`. |
| `record` reports no approved rows | Set `decision` to `approve` in the CSV first. |