{"id":50960029,"url":"https://github.com/ivan-magda/blog-amplifier","last_synced_at":"2026-06-18T12:30:46.000Z","repository":{"id":364647271,"uuid":"1268708506","full_name":"ivan-magda/blog-amplifier","owner":"ivan-magda","description":"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.","archived":false,"fork":false,"pushed_at":"2026-06-13T21:21:50.000Z","size":148,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-13T23:10:53.131Z","etag":null,"topics":["apify","blogging","claude","cli","content-marketing","developer-tools","linkedin","nodejs","social-media","twitter","typescript","web-scraping"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ivan-magda.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-13T21:04:59.000Z","updated_at":"2026-06-13T21:21:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ivan-magda/blog-amplifier","commit_stats":null,"previous_names":["ivan-magda/blog-amplifier"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/ivan-magda/blog-amplifier","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ivan-magda%2Fblog-amplifier","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ivan-magda%2Fblog-amplifier/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ivan-magda%2Fblog-amplifier/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ivan-magda%2Fblog-amplifier/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ivan-magda","download_url":"https://codeload.github.com/ivan-magda/blog-amplifier/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ivan-magda%2Fblog-amplifier/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34491225,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-18T02:00:06.871Z","response_time":128,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["apify","blogging","claude","cli","content-marketing","developer-tools","linkedin","nodejs","social-media","twitter","typescript","web-scraping"],"created_at":"2026-06-18T12:30:43.541Z","updated_at":"2026-06-18T12:30:45.993Z","avatar_url":"https://github.com/ivan-magda.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# blog-amplifier\n\nFind 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.\n\n## Why it works this way\n\nScrapers 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:\n\n- **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.\n- **A human gate.** The judge writes each candidate to `data/review-queue.csv`. Nothing moves forward until you set the `decision` column yourself.\n- **Manual posting.** `record` prints a checklist. You post the approved comments at your own pace.\n- **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.\n\n## Requirements\n\n- Node 20.12 or newer. The CLI loads `.env` with `process.loadEnvFile`, so there is no `dotenv` dependency.\n- An Apify token, from https://console.apify.com/account/integrations.\n- The `claude` CLI, installed and logged in. The judge runs `claude -p` against your Claude Code subscription, so no metered API key is involved.\n\n## Setup\n\n```sh\nnpm install\ncp .env.example .env      # then set APIFY_TOKEN\nclaude                    # then run /login once, to authenticate the judge\n```\n\nTwo optional judge overrides go in `.env`: `JUDGE_MODEL` (default `sonnet`) and `JUDGE_TIMEOUT_MS` (default `180000`).\n\n## Quick start\n\nPromote a GitHub repo across both platforms:\n\n```sh\nnpm run add-subject -- --repo acme/widget-cli\nnpm run subjects                              # confirm the subject id\nnpm run pipeline -- --subject widget-cli      # discover + judge\n# open data/review-queue.csv and set `decision` to approve or reject\nnpm run record                                # prints what to post\n```\n\nPromote a blog post on X only:\n\n```sh\nnpm run add-subject -- --blog ../blog/posts/rate-limiting-explained.md\nnpm run pipeline -- --subject rate-limiting-explained --platform x\n# review data/review-queue.csv, then:\nnpm run record\n```\n\nPut flags after `--` when you run an npm script. To call the CLI directly, drop the `--`: `tsx src/cli.ts pipeline --subject widget-cli`.\n\n## Commands\n\n| Command | What it does |\n|---|---|\n| `add-subject --blog \u003cpath\u003e` or `--repo \u003cowner/name\u003e` | Read metadata and write `subjects/\u003cid\u003e.json`. |\n| `subjects` | List saved subject ids. |\n| `discover --subject \u003cid\u003e [--platform x\\|linkedin\\|both]` | Run the actors, drop URLs you already actioned, and write `data/candidates/\u003cid\u003e-\u003cts\u003e.candidates.json`. Default platform is both. |\n| `judge --run \u003cid\\|latest\\|path\u003e` | Score relevance, draft comments, write `data/queue/\u003cid\u003e-\u003cts\u003e.scored.json`, and append rows to `data/review-queue.csv`. |\n| `record` | Read the approved rows, append them to the ledger, and print a manual posting checklist. |\n| `pipeline --subject \u003cid\u003e [--platform ...]` | Run `discover` then `judge` in one step. |\n\nFor usage text, run `tsx src/cli.ts help` (there is no `npm run help` script).\n\nWhich 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.\n\n## The review gate\n\n`judge` writes one row per candidate to `data/review-queue.csv`. Open it in a spreadsheet or editor and set two columns:\n\n- `decision`: `approve`, `reject`, or blank for pending.\n- `final_comment`: optional. Leave it blank to use the AI `draft_comment`.\n\n`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.\n\n## How ranking works\n\n```\nscore = 0.6 · (relevance / 100) + 0.25 · engagement + 0.15 · recency\n```\n\nRelevance 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`.\n\n## Tuning a subject\n\n`add-subject` writes a starting query, but you will get better results by editing `subjects/\u003cid\u003e.json`. Each subject holds a per-platform query under `queries`:\n\n```json\n\"queries\": {\n  \"x\": \"(\\\"rate limiting\\\" OR \\\"token bucket\\\") lang:en\",\n  \"linkedin\": \"rate limiting token bucket api\"\n}\n```\n\nWrite 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.\n\n## Data layout\n\n```\nsubjects/\u003cid\u003e.json                          the subject you edit (keywords, per-platform queries)\ndata/candidates/\u003cid\u003e-\u003cts\u003e.candidates.json   normalized candidates from one discover run\ndata/queue/\u003cid\u003e-\u003cts\u003e.scored.json            ranked candidates with drafts\ndata/review-queue.csv                       the human gate you edit\ndata/actions.log.jsonl                      append-only ledger for idempotency and dedup\n```\n\n## Apify on the free plan\n\nThe defaults run on Apify's free plan:\n\n- 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.\n- LinkedIn uses `harvestapi/linkedin-post-search`, from about $1.50 per 1,000 results, with no cookies or account.\n\nBoth 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.\n\n## Troubleshooting\n\n| Symptom | Cause and fix |\n|---|---|\n| `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. |\n| `APIFY_TOKEN is not set` | Set it in `.env`. |\n| `Failed to spawn claude` | Install the `claude` CLI and run `/login`. |\n| `record` reports no approved rows | Set `decision` to `approve` in the CSV first. |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fivan-magda%2Fblog-amplifier","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fivan-magda%2Fblog-amplifier","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fivan-magda%2Fblog-amplifier/lists"}