social-autoposter 1.1.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -55
- package/SKILL.md +58 -10
- package/bin/auth.js +110 -0
- package/bin/cli.js +306 -70
- package/bin/cookie-helper.js +315 -0
- package/bin/platform.js +64 -0
- package/bin/scheduler/index.js +14 -0
- package/bin/scheduler/launchd.js +518 -0
- package/bin/scheduler/systemd.js +313 -0
- package/bin/server.js +13850 -776
- package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
- package/browser-agent-configs/linkedin-agent.json +17 -0
- package/browser-agent-configs/reddit-agent-mcp.json +16 -0
- package/browser-agent-configs/reddit-agent.json +17 -0
- package/browser-agent-configs/twitter-agent-mcp.json +16 -0
- package/browser-agent-configs/twitter-agent.json +17 -0
- package/package.json +19 -6
- package/requirements.txt +7 -0
- package/schema-postgres.sql +213 -0
- package/scripts/_dm_icp_batch.py +44 -0
- package/scripts/_gsc_roi_query.py +231 -0
- package/scripts/_insert_post_013.py +125 -0
- package/scripts/_li_discover_pending.py +157 -0
- package/scripts/_log_cyrano_apartmenthacks.py +54 -0
- package/scripts/_seo_lane_roi.py +340 -0
- package/scripts/_serp_report.py +223 -0
- package/scripts/_serp_vs_gsc_report.py +253 -0
- package/scripts/active_campaigns.py +110 -0
- package/scripts/add_deploy_metadata.py +70 -0
- package/scripts/amplitude_24h_signups.py +468 -0
- package/scripts/amplitude_signups.py +177 -0
- package/scripts/amplitude_user_lookup.py +221 -0
- package/scripts/audit_signup_wiring.py +202 -0
- package/scripts/backfill_aggregate_stats.py +143 -0
- package/scripts/backfill_claude_session_subagents.py +290 -0
- package/scripts/backfill_ensure_dms.py +132 -0
- package/scripts/backfill_icp_precheck.py +231 -0
- package/scripts/backfill_linkedin_activity_urns.py +448 -0
- package/scripts/backfill_mk0r_get_started.py +163 -0
- package/scripts/backfill_real_clicks.py +257 -0
- package/scripts/backfill_run_monitor.py +299 -0
- package/scripts/backfill_seo_authors.py +241 -0
- package/scripts/backfill_seo_engagement.py +299 -0
- package/scripts/backfill_target_project.py +112 -0
- package/scripts/backfill_twitter_log_post_no_id.py +322 -0
- package/scripts/batch_send_dms.py +144 -0
- package/scripts/blog_refactor_single_route.py +196 -0
- package/scripts/campaign_bump.py +54 -0
- package/scripts/check_analytics_wiring.py +873 -0
- package/scripts/check_backfill_replied.py +107 -0
- package/scripts/check_contrast.py +425 -0
- package/scripts/check_deploy_wiring.py +138 -0
- package/scripts/check_improve_runs.py +32 -0
- package/scripts/check_layout_wiring.py +287 -0
- package/scripts/check_link_rules.py +101 -0
- package/scripts/check_pep604_annotations.py +163 -0
- package/scripts/check_unread_web_chats.py +108 -0
- package/scripts/claim_web_chat.py +67 -0
- package/scripts/classify_all_dms.py +163 -0
- package/scripts/cleanup_moltbook_dupes_16060.py +146 -0
- package/scripts/cohort_score_distribution.py +112 -0
- package/scripts/daily_stats_email.py +321 -0
- package/scripts/db.py +100 -11
- package/scripts/discover_linkedin_candidates.py +896 -0
- package/scripts/dm_conversation.py +1181 -24
- package/scripts/dm_helper.py +47 -0
- package/scripts/dm_outreach_helper.py +147 -0
- package/scripts/dm_send_log.py +99 -0
- package/scripts/dm_short_links.py +918 -0
- package/scripts/dump_twitter_storage.py +25 -0
- package/scripts/dump_web_chat_history.py +86 -0
- package/scripts/engage_github.py +566 -0
- package/scripts/engage_reddit.py +1156 -0
- package/scripts/engagement_styles.py +884 -0
- package/scripts/engagement_styles_extra.json +65 -0
- package/scripts/enrich_twitter_candidates.py +82 -0
- package/scripts/export_cdp_storage_state.py +190 -0
- package/scripts/extract_user_messages_today.py +291 -0
- package/scripts/fazm_seo_health.py +109 -0
- package/scripts/fetch_prospect_profile.py +226 -0
- package/scripts/fetch_twitter_t1.py +122 -0
- package/scripts/find_threads.py +81 -29
- package/scripts/fix_mdx_light_mode.py +73 -0
- package/scripts/fix_svg_paragraph_wrap.py +59 -0
- package/scripts/funnel_per_day.py +194 -0
- package/scripts/generation_trace.py +124 -0
- package/scripts/get_run_cost.py +113 -0
- package/scripts/github_tools.py +348 -0
- package/scripts/historical_engagement.py +96 -0
- package/scripts/http_api.py +134 -0
- package/scripts/identity.py +248 -0
- package/scripts/ig_collate_transcripts.py +62 -0
- package/scripts/ig_post_type_picker.py +114 -0
- package/scripts/ingest_human_dm_replies.py +253 -0
- package/scripts/ingest_human_seo_replies.py +289 -0
- package/scripts/ingest_web_chat_replies.py +242 -0
- package/scripts/install_lane_digest.py +222 -0
- package/scripts/install_lane_monitor.py +141 -0
- package/scripts/li_discover_insert.py +173 -0
- package/scripts/li_process_notifications.py +153 -0
- package/scripts/link_tail.py +370 -0
- package/scripts/linkedin_api.py +554 -0
- package/scripts/linkedin_browser.py +585 -0
- package/scripts/linkedin_cooldown.py +128 -0
- package/scripts/linkedin_url.py +253 -0
- package/scripts/log_claude_session.py +649 -0
- package/scripts/log_draft.py +94 -0
- package/scripts/log_linkedin_search_attempts.py +117 -0
- package/scripts/log_post.py +351 -0
- package/scripts/log_run.py +206 -0
- package/scripts/log_twitter_search_attempts.py +84 -0
- package/scripts/log_twitter_skips.py +236 -0
- package/scripts/lookup_post.py +89 -0
- package/scripts/mark_web_chat_processed.py +45 -0
- package/scripts/mcp_lock_proxy.py +370 -0
- package/scripts/migrate_dm_links.py +128 -0
- package/scripts/migrate_link_clicks.py +97 -0
- package/scripts/migrate_post_links.py +88 -0
- package/scripts/migrate_replies_stats.py +45 -0
- package/scripts/migrate_subreddit_bans_to_objects.py +113 -0
- package/scripts/moltbook_post.py +25 -5
- package/scripts/moltbook_tools.py +159 -0
- package/scripts/octolens_threads.py +10 -1
- package/scripts/octolens_twitter_batch.py +209 -0
- package/scripts/octolens_twitter_cdp.py +165 -0
- package/scripts/pending_threads.py +277 -0
- package/scripts/phase_d_new_comments.py +2 -2
- package/scripts/pick_project.py +52 -10
- package/scripts/pick_thread_target.py +251 -0
- package/scripts/pick_twitter_thread_target.py +245 -0
- package/scripts/poll_web_chat.py +72 -0
- package/scripts/post_github.py +1128 -0
- package/scripts/post_reddit.py +2287 -0
- package/scripts/precompute_dashboard_stats.py +298 -0
- package/scripts/progress.py +88 -0
- package/scripts/project_deploy_status.py +272 -0
- package/scripts/project_excludes.py +359 -0
- package/scripts/project_slugs.py +91 -0
- package/scripts/project_stats.py +23 -13
- package/scripts/project_stats_json.py +1207 -0
- package/scripts/promote_engagement_styles.py +209 -0
- package/scripts/reddit_browser.py +2141 -0
- package/scripts/reddit_browser_lock.py +571 -0
- package/scripts/reddit_chat_sync.py +710 -0
- package/scripts/reddit_tools.py +835 -0
- package/scripts/reply_db.py +134 -17
- package/scripts/reply_insert.py +98 -0
- package/scripts/ripen_reddit_plan.py +478 -0
- package/scripts/run_moltbook_cycle.py +540 -0
- package/scripts/scan_dm_candidates.py +254 -22
- package/scripts/scan_moltbook_replies.py +246 -0
- package/scripts/scan_reddit_replies.py +377 -0
- package/scripts/scan_twitter_mentions_browser.py +233 -0
- package/scripts/scan_twitter_thread_followups.py +243 -0
- package/scripts/score_linkedin_candidates.py +417 -0
- package/scripts/score_twitter_candidates.py +307 -0
- package/scripts/scrape_linkedin_comment_stats.py +495 -0
- package/scripts/scrape_linkedin_stats_browser.py +52 -0
- package/scripts/scrape_reddit_views.py +169 -49
- package/scripts/send_comment_replies.py +110 -0
- package/scripts/send_web_chat_reply.py +181 -0
- package/scripts/seo_health_all_projects.py +94 -0
- package/scripts/socialcrawl.py +116 -0
- package/scripts/strike_alert.py +257 -0
- package/scripts/sweep_guide_chrome.py +212 -0
- package/scripts/sweep_post_link_clicks.py +545 -0
- package/scripts/sync_web_chat_config.py +144 -0
- package/scripts/test_own_reply_dedup.py +56 -0
- package/scripts/top_dud_linkedin_queries.py +90 -0
- package/scripts/top_dud_reddit_queries.py +67 -0
- package/scripts/top_dud_twitter_queries.py +85 -0
- package/scripts/top_linkedin_queries.py +68 -0
- package/scripts/top_omitted_reddit_topics.py +91 -0
- package/scripts/top_performers.py +379 -47
- package/scripts/top_search_topics.py +289 -0
- package/scripts/top_twitter_queries.py +199 -0
- package/scripts/twitter_batch_phase.py +144 -0
- package/scripts/twitter_browser.py +1779 -0
- package/scripts/twitter_compose_dm.py +332 -0
- package/scripts/twitter_gen_links.py +226 -0
- package/scripts/twitter_post_plan.py +493 -0
- package/scripts/twitter_supply_signal.py +102 -0
- package/scripts/unclaim_web_chat.py +40 -0
- package/scripts/update_linkedin_stats_from_feed.py +355 -0
- package/scripts/update_stats.py +1338 -196
- package/scripts/watchdog_hung_runs.py +318 -0
- package/scripts/write_generation_trace.py +73 -0
- package/setup/SKILL.md +12 -15
- package/skill/engage.sh +68 -220
- package/skill/run-github.sh +73 -88
- package/skill/run-linkedin.sh +828 -59
- package/skill/run-moltbook.sh +22 -55
- package/skill/run-reddit-search.sh +476 -0
- package/skill/run-reddit-threads.sh +791 -0
- package/skill/run-twitter-cycle.sh +1211 -0
- package/skill/stats.sh +423 -264
- package/.env.example +0 -11
- package/scripts/log_fazm_linkedin.py +0 -62
- package/scripts/log_fazm_linkedin_batch2.py +0 -111
- package/scripts/phase_d_resolve.py +0 -226
- package/scripts/recover_linkedin_urls.py +0 -301
- package/scripts/scan_replies.py +0 -547
- package/scripts/scrape_linkedin_stats.py +0 -173
- package/skill/run-reddit.sh +0 -56
- package/skill/run-twitter.sh +0 -57
package/README.md
CHANGED
|
@@ -1,84 +1,139 @@
|
|
|
1
1
|
# social-autoposter
|
|
2
2
|
|
|
3
|
-
Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook.
|
|
3
|
+
Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Ships as a Claude Code skill plus a set of standalone Python helpers and macOS launchd jobs.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Posts are written to a Neon Postgres database via `DATABASE_URL` in `~/social-autoposter/.env`. Bring your own Neon DB and apply `schema-postgres.sql` once. Each platform drives its own persistent Playwright MCP browser profile, so logins survive across runs.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
A new machine needs all of these before the pipeline can run end to end:
|
|
10
|
+
|
|
11
|
+
- **macOS** (the launchd plists are mac-only; Linux users can crib the cron snippets from `setup/SKILL.md` Step 7)
|
|
12
|
+
- **Node.js 16+** (for `npx`, the installer, and `@playwright/mcp` at runtime)
|
|
13
|
+
- **Python 3.9+** with `pip3` (helper scripts; `psycopg2-binary` is auto-installed by the installer)
|
|
14
|
+
- **Claude Code CLI** on `PATH` (the cron scripts shell out to `claude -p` with a per-platform MCP config)
|
|
15
|
+
- **`psql`** on `PATH` (a few scripts query Neon directly)
|
|
16
|
+
- One Chromium install per platform (created on first run by `@playwright/mcp` against the persistent profile dirs)
|
|
17
|
+
|
|
18
|
+
Optional:
|
|
19
|
+
|
|
20
|
+
- `MOLTBOOK_API_KEY` in `.env` for Moltbook posting and scanning
|
|
21
|
+
- `RESEND_API_KEY` and `NOTIFICATION_EMAIL` in `.env` for DM-escalation emails
|
|
22
|
+
|
|
23
|
+
## Install
|
|
8
24
|
|
|
9
25
|
```bash
|
|
10
26
|
npx social-autoposter init
|
|
11
27
|
```
|
|
12
28
|
|
|
13
|
-
|
|
29
|
+
`bin/cli.js` does all of the wiring in one shot:
|
|
30
|
+
|
|
31
|
+
1. Copies `scripts/`, `skill/`, `setup/`, `SKILL.md`, `schema-postgres.sql`, and `browser-agent-configs/` into `~/social-autoposter/`
|
|
32
|
+
2. Creates `config.json` from `config.example.json` and writes a blank `.env` template (fill in your own `DATABASE_URL` and optional `MOLTBOOK_API_KEY`)
|
|
33
|
+
3. Installs `psycopg2-binary` via `pip3` if missing
|
|
34
|
+
4. Generates launchd plists in `~/social-autoposter/launchd/` with the user's actual `HOME` and `PATH`
|
|
35
|
+
5. Installs the Playwright MCP configs to `~/.claude/browser-agent-configs/` (twitter, reddit, linkedin) with `__HOME__` and `__NODE_BIN__` placeholders substituted. Existing files are left alone, so any window-position tweaks survive `npx social-autoposter update`.
|
|
36
|
+
6. Creates empty persistent browser profile dirs at `~/.claude/browser-profiles/{twitter,reddit,linkedin}`
|
|
37
|
+
7. Symlinks `~/.claude/skills/social-autoposter` and `~/.claude/skills/social-autoposter-setup` to the install dir
|
|
38
|
+
|
|
39
|
+
To refresh code without touching user files (`config.json`, `.env`, `SKILL.md`, or any browser config you customized):
|
|
14
40
|
|
|
15
|
-
To update scripts without touching your config or database:
|
|
16
41
|
```bash
|
|
17
42
|
npx social-autoposter update
|
|
18
43
|
```
|
|
19
44
|
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
cp config.example.json config.json # edit with your accounts
|
|
23
|
-
psql "$DATABASE_URL" -f schema-postgres.sql # initialize the Neon DB
|
|
24
|
-
bash setup.sh # symlinks + launchd (macOS)
|
|
25
|
-
```
|
|
45
|
+
## Configure
|
|
26
46
|
|
|
27
|
-
|
|
47
|
+
Tell your Claude Code agent: **"set up social autoposter"**. The interactive wizard in `setup/SKILL.md` walks through:
|
|
28
48
|
|
|
49
|
+
1. Verifying the Neon connection
|
|
50
|
+
2. Filling in `~/social-autoposter/config.json` with handles for Reddit, Twitter, LinkedIn, optional Moltbook
|
|
51
|
+
3. A 5-question interview to draft your `content_angle`
|
|
52
|
+
4. Capturing `projects` with `topics` (used by the tiered reply strategy)
|
|
53
|
+
5. Verifying browser logins per platform via the dedicated MCP agent. The first time each platform runs you'll be asked to log in once; cookies persist into the userDataDir under `~/.claude/browser-profiles/`.
|
|
54
|
+
6. A dry-run of `find_threads.py --limit 3`
|
|
55
|
+
7. Optional: loading the launchd plists into `~/Library/LaunchAgents/`
|
|
56
|
+
|
|
57
|
+
## How the runtime is wired
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
launchd ──▶ skill/run-{platform}.sh ──▶ claude -p --strict-mcp-config --mcp-config ~/.claude/browser-agent-configs/{platform}-agent-mcp.json
|
|
61
|
+
│ │
|
|
62
|
+
│ └──▶ @playwright/mcp@latest
|
|
63
|
+
│ │
|
|
64
|
+
│ └──▶ ~/.claude/browser-profiles/{platform}/ (persistent userDataDir)
|
|
65
|
+
│
|
|
66
|
+
├──▶ scripts/find_threads.py, top_twitter_queries.py (no browser, API + DB dedup)
|
|
67
|
+
├──▶ scripts/pick_project.py (weighted project rotation)
|
|
68
|
+
├──▶ scripts/top_performers.py (feedback report from past stats)
|
|
69
|
+
└──▶ Neon Postgres (DATABASE_URL in .env)
|
|
29
70
|
```
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
71
|
+
|
|
72
|
+
Each `skill/run-*.sh`:
|
|
73
|
+
|
|
74
|
+
1. Controlled by launchd (load/unload). Use the dashboard Pause All / Resume All button, or `launchctl unload/load` directly
|
|
75
|
+
2. Acquires a per-platform lock from `skill/lock.sh` (waits up to 60 min for any prior run)
|
|
76
|
+
3. Sources `~/social-autoposter/.env`
|
|
77
|
+
4. Picks a project, builds a feedback report, fetches `llms.txt` for product context
|
|
78
|
+
5. Calls `find_*.py` for API-side candidates already deduped against the DB
|
|
79
|
+
6. Spawns a child Claude process with `--strict-mcp-config` so it only sees the one platform's browser MCP
|
|
80
|
+
|
|
81
|
+
The launchd schedules generated by `bin/cli.js` on install:
|
|
82
|
+
|
|
83
|
+
| Job | Cadence |
|
|
84
|
+
|-----|---------|
|
|
85
|
+
| `com.m13v.social-stats` (`stats.sh`) | every 21600 s (6 h) |
|
|
86
|
+
| `com.m13v.social-engage` (`engage.sh`) | every 21600 s (6 h) |
|
|
87
|
+
|
|
88
|
+
All per-platform plists live in `launchd/` (reddit-search, reddit-threads, twitter-cycle, linkedin, moltbook, github, octolens, audit, dm-replies-*, link-edit-*, scan-reddit-replies, scan-moltbook-replies, etc.) and use either `StartInterval` or `StartCalendarInterval` for fixed wall-clock times. Activate any of them with:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
ln -sf ~/social-autoposter/launchd/com.m13v.social-twitter-cycle.plist ~/Library/LaunchAgents/
|
|
92
|
+
launchctl load ~/Library/LaunchAgents/com.m13v.social-twitter-cycle.plist
|
|
44
93
|
```
|
|
45
94
|
|
|
46
|
-
##
|
|
95
|
+
## Skill commands
|
|
96
|
+
|
|
97
|
+
| Command | What it does |
|
|
98
|
+
|---------|-------------|
|
|
99
|
+
| `/social-autoposter` | Comment run: find threads, draft, post, log (cron-safe) |
|
|
100
|
+
| `/social-autoposter post` | Create an original post or thread (manual only) |
|
|
101
|
+
| `/social-autoposter stats` | Update engagement stats via API |
|
|
102
|
+
| `/social-autoposter engage` | Scan and reply to responses on our posts |
|
|
103
|
+
| `/social-autoposter audit` | Full browser audit of all posts |
|
|
104
|
+
|
|
105
|
+
View live stats at `https://s4l.ai/stats/<your-handle>` once posts start landing in Neon.
|
|
106
|
+
|
|
107
|
+
## Repo layout
|
|
47
108
|
|
|
48
109
|
```
|
|
49
110
|
social-autoposter/
|
|
50
|
-
├── SKILL.md
|
|
51
|
-
├──
|
|
52
|
-
├──
|
|
53
|
-
├──
|
|
54
|
-
├──
|
|
55
|
-
|
|
56
|
-
├── scripts/
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
│ ├── scan_replies.py <- scan for new replies to our posts via API
|
|
60
|
-
│ └── update_stats.py <- fetch engagement stats via API
|
|
61
|
-
├── skill/
|
|
62
|
-
│ ├── SKILL.md <- personal skill (hardcoded accounts)
|
|
63
|
-
│ ├── run.sh <- hourly posting (launchd wrapper)
|
|
64
|
-
│ ├── stats.sh <- 6-hourly stats (launchd wrapper)
|
|
65
|
-
│ ├── engage.sh <- 2-hourly engagement (launchd wrapper)
|
|
66
|
-
│ └── logs/ <- runtime logs (gitignored)
|
|
67
|
-
└── launchd/ <- macOS LaunchAgent plists
|
|
111
|
+
├── SKILL.md the playbook (locked, immutable)
|
|
112
|
+
├── bin/cli.js installer + dashboard launcher
|
|
113
|
+
├── browser-agent-configs/ Playwright MCP templates (twitter/reddit/linkedin)
|
|
114
|
+
├── config.example.json config template
|
|
115
|
+
├── schema-postgres.sql Neon schema
|
|
116
|
+
├── setup/SKILL.md interactive setup wizard skill (locked)
|
|
117
|
+
├── scripts/ Python and JS helpers (no browser, no LLM)
|
|
118
|
+
├── skill/ shell wrappers invoked by launchd
|
|
119
|
+
└── launchd/ generated macOS LaunchAgent plists
|
|
68
120
|
```
|
|
69
121
|
|
|
70
122
|
## For other AI agents
|
|
71
123
|
|
|
72
|
-
The skill
|
|
73
|
-
- **Shell access** (to run Python scripts and psql)
|
|
74
|
-
- **Browser automation** (Playwright, Selenium, etc. for posting)
|
|
75
|
-
- **An LLM** (for drafting comments in the right tone)
|
|
124
|
+
The skill works with any agent that has shell access, browser automation, and an LLM. The Python and JS helpers in `scripts/` handle thread discovery, reply scanning, and stats updates without needing a browser. `SKILL.md` is the playbook; any agent can read it and execute the workflows with its own tools.
|
|
76
125
|
|
|
77
|
-
|
|
126
|
+
## Pause and resume
|
|
78
127
|
|
|
79
|
-
|
|
128
|
+
Use the dashboard at `localhost:3141` (Pause All / Resume All button), or manually:
|
|
80
129
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
130
|
+
```bash
|
|
131
|
+
# Pause: unload all jobs + kill running processes
|
|
132
|
+
for plist in ~/Library/LaunchAgents/com.m13v.social-*.plist; do launchctl unload "$plist"; done
|
|
133
|
+
|
|
134
|
+
# Resume: reload all jobs
|
|
135
|
+
for plist in ~/social-autoposter/launchd/com.m13v.social-*.plist; do
|
|
136
|
+
ln -sf "$plist" ~/Library/LaunchAgents/
|
|
137
|
+
launchctl load ~/Library/LaunchAgents/$(basename "$plist")
|
|
138
|
+
done
|
|
139
|
+
```
|
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: social-autoposter
|
|
3
|
-
description: "Automate social media posting across Reddit, X/Twitter, LinkedIn, and Moltbook. Find threads, post comments, create original posts, track engagement stats. Use when: 'post to social', 'social autoposter', 'find threads to comment on', 'create a post', 'audit social posts', 'update post stats'
|
|
3
|
+
description: "Automate social media posting across Reddit, X/Twitter, LinkedIn, and Moltbook. Find threads, post comments, create original posts, track engagement stats. Use when: 'post to social', 'social autoposter', 'find threads to comment on', 'create a post', 'audit social posts', 'update post stats'."
|
|
4
4
|
user_invocable: true
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ Automates finding, posting, and tracking social media comments and original post
|
|
|
13
13
|
| Command | What it does |
|
|
14
14
|
|---------|-------------|
|
|
15
15
|
| `/social-autoposter` | Comment run — find threads + post comment + log (cron-safe) |
|
|
16
|
-
| `/social-autoposter post` | Create an original post/thread (manual
|
|
16
|
+
| `/social-autoposter post` | Create an original post/thread (manual or cron-driven for Reddit threads) |
|
|
17
17
|
| `/social-autoposter stats` | Update engagement stats via API |
|
|
18
18
|
| `/social-autoposter engage` | Scan and reply to responses on our posts |
|
|
19
19
|
| `/social-autoposter audit` | Full browser audit of all posts |
|
|
@@ -53,7 +53,8 @@ Standalone Python scripts — no LLM needed.
|
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
55
|
python3 ~/social-autoposter/scripts/find_threads.py --include-moltbook
|
|
56
|
-
python3 ~/social-autoposter/scripts/
|
|
56
|
+
python3 ~/social-autoposter/scripts/scan_reddit_replies.py
|
|
57
|
+
python3 ~/social-autoposter/scripts/scan_moltbook_replies.py
|
|
57
58
|
python3 ~/social-autoposter/scripts/update_stats.py --quiet
|
|
58
59
|
```
|
|
59
60
|
|
|
@@ -121,15 +122,17 @@ Verify: fetch post by UUID, check `verification_status` is `"verified"`.
|
|
|
121
122
|
```sql
|
|
122
123
|
INSERT INTO posts (platform, thread_url, thread_author, thread_author_handle,
|
|
123
124
|
thread_title, thread_content, our_url, our_content, our_account,
|
|
124
|
-
source_summary, project_name, status, posted_at)
|
|
125
|
-
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'active', NOW());
|
|
125
|
+
source_summary, project_name, engagement_style, feedback_report_used, status, posted_at)
|
|
126
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, TRUE, 'active', NOW());
|
|
126
127
|
```
|
|
127
128
|
|
|
128
129
|
Set `project_name` to the matching project name from `config.json` (e.g., 'Fazm', 'Cyrano', 'Terminator'). Every post/comment MUST be labeled with its target project. If engagement is general/unrelated to any project, use 'general'.
|
|
129
130
|
|
|
131
|
+
Set `engagement_style` to the style you chose for this post (e.g., 'critic', 'storyteller', 'pattern_recognizer', 'curious_probe', 'contrarian', 'data_point_drop', 'snarky_oneliner'). Every post MUST have an engagement_style.
|
|
132
|
+
|
|
130
133
|
Use the account value from `config.json` for `our_account`.
|
|
131
134
|
|
|
132
|
-
|
|
135
|
+
Posts are written directly to the Neon Postgres database. No separate post-sync step is required.
|
|
133
136
|
|
|
134
137
|
---
|
|
135
138
|
|
|
@@ -176,10 +179,12 @@ Choose the single best subreddit from `config.json → subreddits` for this topi
|
|
|
176
179
|
```sql
|
|
177
180
|
INSERT INTO posts (platform, thread_url, thread_author, thread_author_handle,
|
|
178
181
|
thread_title, thread_content, our_url, our_content, our_account,
|
|
179
|
-
source_summary, project_name, status, posted_at)
|
|
180
|
-
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'active', NOW());
|
|
182
|
+
source_summary, project_name, engagement_style, feedback_report_used, status, posted_at)
|
|
183
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, TRUE, 'active', NOW());
|
|
181
184
|
```
|
|
182
185
|
|
|
186
|
+
Set `engagement_style` to the style you chose (e.g., 'critic', 'storyteller', 'pattern_recognizer'). Every post MUST have an engagement_style.
|
|
187
|
+
|
|
183
188
|
Set `project_name` to the matching project name from `config.json`. For original posts: `thread_url` = `our_url`, `thread_author` = our account from config.json.
|
|
184
189
|
|
|
185
190
|
### 6. Mandatory engagement plan
|
|
@@ -192,13 +197,39 @@ After posting, you MUST:
|
|
|
192
197
|
|
|
193
198
|
---
|
|
194
199
|
|
|
200
|
+
## Workflow: Cron-driven Reddit Threads (`run-reddit-threads.sh`)
|
|
201
|
+
|
|
202
|
+
Daily-cadence original Reddit threads across all products, automated via launchd.
|
|
203
|
+
|
|
204
|
+
**Config lives per-project** under `projects[].threads`:
|
|
205
|
+
- `enabled`: true/false
|
|
206
|
+
- `own_community`: `{subreddit, cadence, floor_days}` (optional). Defaults to 1-day floor.
|
|
207
|
+
- `external_subreddits`: list of external subs (default 3-day floor, override via `external_floor_days`)
|
|
208
|
+
- `topic_angles`: discussion-starter ideas the agent picks from
|
|
209
|
+
- Voice guidance comes from `projects[].voice` (tone, never)
|
|
210
|
+
- `content_sources.guide_dir` / `link_base`: optional source paths/URLs
|
|
211
|
+
- `dynamic_context.day_counter` / `static_facts`: live-calculated facts injected into the prompt
|
|
212
|
+
|
|
213
|
+
**Source research** uses `landing_pages.repo` + `landing_pages.product_source[]` (same schema as the SEO pipeline). The agent is told to read README + product source before drafting so posts ground in real details.
|
|
214
|
+
|
|
215
|
+
**Picker** (`scripts/pick_thread_target.py`): weighted project selection with:
|
|
216
|
+
- Per-sub floor-days filter (queries `posts` table for this account's last original thread)
|
|
217
|
+
- `subreddit_bans` filter: `banned` (can't post or comment) + `skip_threads` (threads blocked, comments OK)
|
|
218
|
+
- Own-community candidates always picked first when eligible
|
|
219
|
+
|
|
220
|
+
**Schedule**: `com.m13v.social-reddit-threads.plist` fires 4x/day at 00:15, 06:15, 12:15, 18:15.
|
|
221
|
+
|
|
222
|
+
**Lock**: the runner calls `acquire_lock reddit-threads` to serialize against the comment pipeline.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
195
226
|
## Workflow: Stats (`/social-autoposter stats`)
|
|
196
227
|
|
|
197
228
|
```bash
|
|
198
229
|
python3 ~/social-autoposter/scripts/update_stats.py
|
|
199
230
|
```
|
|
200
231
|
|
|
201
|
-
After running, view updated stats at `https://s4l.ai/stats/[handle]`.
|
|
232
|
+
After running, view updated stats at `https://s4l.ai/stats/[handle]`. Stats are read from the same Neon Postgres database used by the posting pipeline. Changes appear on the website within ~5 minutes.
|
|
202
233
|
|
|
203
234
|
---
|
|
204
235
|
|
|
@@ -206,7 +237,8 @@ After running, view updated stats at `https://s4l.ai/stats/[handle]`. The DB syn
|
|
|
206
237
|
|
|
207
238
|
### Phase A: Scan for replies (no browser)
|
|
208
239
|
```bash
|
|
209
|
-
python3 ~/social-autoposter/scripts/
|
|
240
|
+
python3 ~/social-autoposter/scripts/scan_reddit_replies.py
|
|
241
|
+
python3 ~/social-autoposter/scripts/scan_moltbook_replies.py
|
|
210
242
|
```
|
|
211
243
|
|
|
212
244
|
### Phase B: Respond to pending replies
|
|
@@ -275,6 +307,22 @@ GOOD title: "just did my 7th course, some things that surprised me"
|
|
|
275
307
|
BAD body: Structured with headers, bold, numbered lists, "As a tech founder..."
|
|
276
308
|
GOOD body: Paragraphs, incomplete thoughts, personal details, casual tone, ends with a genuine question
|
|
277
309
|
|
|
310
|
+
### Bad vs Good (DM Replies)
|
|
311
|
+
|
|
312
|
+
DM replies are texting-style. 1 to 3 sentences. Always reference something specific from the inbound. No unearned call offers, no fabricated links, no time-bound commitments. Booking links only when the matched project has `booking_link_auto_share: true` AND `qualification_status=qualified` on the DM row.
|
|
313
|
+
|
|
314
|
+
BAD: "Hey! I saw your comment on r/startups about agent orchestration. I'd love to share what we're working on, would you be open to a quick call?" (cold-pitch shape, premature call ask)
|
|
315
|
+
GOOD: "yo the point about agents racing on the same file hit home, we solved it with worktrees per agent. what's your setup?"
|
|
316
|
+
|
|
317
|
+
BAD: "Great question! Our product handles exactly that scenario. Check out [link] for more details." (sales register, leading with link in an early DM)
|
|
318
|
+
GOOD: "we hit that too, ended up using the accessibility API route because screenshot-based kept flaking on retina displays"
|
|
319
|
+
|
|
320
|
+
BAD: "Absolutely! Let's do Thursday at 3pm, I'll send an invite." (time-bound commitment, bot has no calendar authority)
|
|
321
|
+
GOOD: "yeah easier to figure it out here, what specifically are you trying to wire up?"
|
|
322
|
+
|
|
323
|
+
BAD: "I totally understand your hesitation. But our solution is different because..." (defensive, pushy rebuttal)
|
|
324
|
+
GOOD: "makes sense, we kicked it around for 6 months before pulling the trigger. what's been the blocker on your end?"
|
|
325
|
+
|
|
278
326
|
---
|
|
279
327
|
|
|
280
328
|
## Tiered Reply Strategy
|
package/bin/auth.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Firebase auth for the dashboard when CLIENT_MODE=1.
|
|
4
|
+
// When CLIENT_MODE is unset (local operator use), authorize() is a no-op
|
|
5
|
+
// and returns a synthetic admin principal so every existing route keeps
|
|
6
|
+
// working unchanged.
|
|
7
|
+
|
|
8
|
+
let _admin = null;
|
|
9
|
+
|
|
10
|
+
function getAdmin() {
|
|
11
|
+
if (_admin) return _admin;
|
|
12
|
+
const admin = require('firebase-admin');
|
|
13
|
+
if (!admin.apps.length) {
|
|
14
|
+
const projectId = process.env.FIREBASE_PROJECT_ID || 's4l-app-prod';
|
|
15
|
+
admin.initializeApp({ projectId });
|
|
16
|
+
}
|
|
17
|
+
_admin = admin;
|
|
18
|
+
return _admin;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const CLIENT_MODE = process.env.CLIENT_MODE === '1';
|
|
22
|
+
|
|
23
|
+
// Routes that require an admin claim even when authenticated.
|
|
24
|
+
// Clients authenticated with only a project scope cannot hit these.
|
|
25
|
+
const ADMIN_ONLY_PATTERNS = [
|
|
26
|
+
/^\/api\/pause$/,
|
|
27
|
+
/^\/api\/resume$/,
|
|
28
|
+
/^\/api\/jobs\/[^/]+\/(toggle|run|stop|interval)$/,
|
|
29
|
+
/^\/api\/phase\/[^/]+\/interval$/,
|
|
30
|
+
/^\/api\/config$/,
|
|
31
|
+
/^\/api\/env$/,
|
|
32
|
+
/^\/api\/logs(\/.*)?$/,
|
|
33
|
+
/^\/api\/webhooks(\/.*)?$/,
|
|
34
|
+
/^\/api\/status$/,
|
|
35
|
+
/^\/api\/pending$/,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function isAdminOnly(pathname) {
|
|
39
|
+
return ADMIN_ONLY_PATTERNS.some(re => re.test(pathname));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extractToken(req) {
|
|
43
|
+
const h = req.headers['authorization'] || req.headers['Authorization'];
|
|
44
|
+
if (!h) return null;
|
|
45
|
+
const m = String(h).match(/^Bearer\s+(.+)$/);
|
|
46
|
+
return m ? m[1].trim() : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function verifyAuth(req, pathname) {
|
|
50
|
+
if (!CLIENT_MODE) {
|
|
51
|
+
return { ok: true, user: { uid: 'local', email: 'local', admin: true, projects: [] } };
|
|
52
|
+
}
|
|
53
|
+
const token = extractToken(req);
|
|
54
|
+
if (!token) {
|
|
55
|
+
console.warn(JSON.stringify({ event: 'auth_reject', reason: 'missing_token', path: pathname, authHeaderPresent: !!(req.headers['authorization'] || req.headers['Authorization']) }));
|
|
56
|
+
return { ok: false, status: 401, error: 'missing_token' };
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const decoded = await getAdmin().auth().verifyIdToken(token);
|
|
60
|
+
const user = {
|
|
61
|
+
uid: decoded.uid,
|
|
62
|
+
email: decoded.email || null,
|
|
63
|
+
admin: decoded.admin === true,
|
|
64
|
+
projects: Array.isArray(decoded.projects) ? decoded.projects : [],
|
|
65
|
+
};
|
|
66
|
+
if (isAdminOnly(pathname) && !user.admin) {
|
|
67
|
+
console.warn(JSON.stringify({ event: 'auth_reject', reason: 'admin_required', path: pathname, uid: user.uid, email: user.email }));
|
|
68
|
+
return { ok: false, status: 403, error: 'admin_required' };
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, user };
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.warn(JSON.stringify({ event: 'auth_reject', reason: 'invalid_token', path: pathname, errCode: e.code || null, errMessage: e.message, tokenHead: token.slice(0, 20), tokenLen: token.length }));
|
|
73
|
+
return { ok: false, status: 401, error: 'invalid_token', detail: e.message };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function scopedProjects(user, requested) {
|
|
78
|
+
if (user.admin) {
|
|
79
|
+
return requested && requested !== 'all' ? [requested] : null;
|
|
80
|
+
}
|
|
81
|
+
if (!user.projects.length) return [];
|
|
82
|
+
if (!requested || requested === 'all') return user.projects.slice();
|
|
83
|
+
return user.projects.includes(requested) ? [requested] : [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Returns { clause, ok } where clause is a " AND <column> IN ('a','b')" fragment
|
|
87
|
+
// (possibly empty when admin + no filter) and ok=false means the user has no
|
|
88
|
+
// access (non-admin with empty projects claim) so the handler should 403.
|
|
89
|
+
// Project names are validated against a conservative charset; single quotes
|
|
90
|
+
// are also SQL-escaped below as defense in depth. Spaces are allowed because
|
|
91
|
+
// real config.json names include them ('WhatsApp MCP', 'AI Browser Profile',
|
|
92
|
+
// 'macOS MCP', 'macOS Session Replay').
|
|
93
|
+
const PROJECT_NAME_RE = /^[A-Za-z0-9 _\-]{1,64}$/;
|
|
94
|
+
|
|
95
|
+
function projectClause(user, column, requested) {
|
|
96
|
+
const list = scopedProjects(user, requested);
|
|
97
|
+
if (list === null) return { clause: '', ok: true }; // admin, unfiltered
|
|
98
|
+
const safe = list.filter(p => PROJECT_NAME_RE.test(p));
|
|
99
|
+
if (!safe.length) return { clause: '', ok: false };
|
|
100
|
+
const quoted = safe.map(p => `'${p.replace(/'/g, "''")}'`).join(',');
|
|
101
|
+
return { clause: ` AND ${column} IN (${quoted})`, ok: true, list: safe };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
CLIENT_MODE,
|
|
106
|
+
verifyAuth,
|
|
107
|
+
isAdminOnly,
|
|
108
|
+
scopedProjects,
|
|
109
|
+
projectClause,
|
|
110
|
+
};
|