social-autoposter 1.6.13 → 1.6.15
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 +6 -6
- package/SKILL.md +3 -3
- package/bin/cli.js +127 -15
- package/bin/server.js +329 -10
- package/package.json +1 -1
- package/requirements.txt +4 -0
- package/schema-postgres.sql +52 -1
- package/scripts/active_campaigns.py +3 -3
- package/scripts/batch_send_dms.py +5 -1
- package/scripts/campaign_bump.py +3 -3
- package/scripts/check_unread_web_chats.py +2 -2
- package/scripts/db.py +7 -6
- package/scripts/dm_short_links.py +4 -4
- package/scripts/engagement_styles.py +173 -2
- package/scripts/generate_daily_human_style.py +314 -0
- package/scripts/get_run_cost.py +2 -2
- package/scripts/heartbeat.sh +1 -1
- package/scripts/ingest_web_chat_replies.py +1 -1
- package/scripts/link_tail.py +2 -5
- package/scripts/log_claude_session.py +5 -5
- package/scripts/log_post.py +11 -0
- package/scripts/phase_d_edit.py +5 -1
- package/scripts/phase_d_new_comments.py +3 -1
- package/scripts/pick_project.py +6 -6
- package/scripts/poll_web_chat.py +1 -1
- package/scripts/precompute_dashboard_stats.py +4 -4
- package/scripts/project_stats_json.py +1 -1
- package/scripts/realign_sequences.py +60 -0
- package/scripts/score_linkedin_candidates.py +7 -1
- package/scripts/scratch_seo_gsc.py +134 -0
- package/scripts/scratch_seo_posthog.py +179 -0
- package/scripts/scratch_seo_volume.py +136 -0
- package/scripts/send_batch_dms.sh +4 -1
- package/scripts/top_performers.py +5 -5
- package/scripts/twitter_browser.py +47 -3
- package/scripts/twitter_post_plan.py +86 -0
- package/setup/SKILL.md +5 -5
- package/skill/check-web-chats.sh +7 -7
- package/skill/github-engage.sh +1 -1
- package/skill/link-edit-github.sh +26 -6
- package/skill/link-edit-linkedin.sh +26 -6
- package/skill/run-generate-daily-style.sh +45 -0
- package/skill/run-instagram-render.sh +2 -2
- package/skill/run-linkedin.sh +1 -1
- package/skill/run-reddit-search.sh +2 -2
- package/skill/run-twitter-cycle-singleton.sh +66 -0
- package/skill/run-twitter-cycle.sh +22 -6
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Open-source repo behind **[S4L (s4lai)](https://s4l.ai)**: an automated social p
|
|
|
4
4
|
|
|
5
5
|
> The hosted managed version is **S4L** (written `s4lai`, domain `s4l.ai`): done-for-you Reddit and Twitter brand-awareness, $1/1K impressions, $50/1K site visits. See https://s4l.ai.
|
|
6
6
|
|
|
7
|
-
Posts are written to a
|
|
7
|
+
Posts are written to a Postgres database via `DATABASE_URL` in `~/social-autoposter/.env`. Bring your own Postgres DB and apply `schema-postgres.sql` once. Each platform drives its own persistent Playwright MCP browser profile, so logins survive across runs.
|
|
8
8
|
|
|
9
9
|
## Prerequisites
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ A new machine needs all of these before the pipeline can run end to end:
|
|
|
14
14
|
- **Node.js 16+** (for `npx`, the installer, and `@playwright/mcp` at runtime)
|
|
15
15
|
- **Python 3.9+** with `pip3` (helper scripts; `psycopg2-binary` is auto-installed by the installer)
|
|
16
16
|
- **Claude Code CLI** on `PATH` (the cron scripts shell out to `claude -p` with a per-platform MCP config)
|
|
17
|
-
- **`psql`** on `PATH` (a few scripts query
|
|
17
|
+
- **`psql`** on `PATH` (a few scripts query Postgres directly)
|
|
18
18
|
- One Chromium install per platform (created on first run by `@playwright/mcp` against the persistent profile dirs)
|
|
19
19
|
|
|
20
20
|
Optional:
|
|
@@ -48,7 +48,7 @@ npx social-autoposter update
|
|
|
48
48
|
|
|
49
49
|
Tell your Claude Code agent: **"set up social autoposter"**. The interactive wizard in `setup/SKILL.md` walks through:
|
|
50
50
|
|
|
51
|
-
1. Verifying the
|
|
51
|
+
1. Verifying the Postgres connection
|
|
52
52
|
2. Filling in `~/social-autoposter/config.json` with handles for Reddit, Twitter, LinkedIn, optional Moltbook
|
|
53
53
|
3. A 5-question interview to draft your `content_angle`
|
|
54
54
|
4. Capturing `projects` with `topics` (used by the tiered reply strategy)
|
|
@@ -68,7 +68,7 @@ launchd ──▶ skill/run-{platform}.sh ──▶ claude -p --strict-mcp-
|
|
|
68
68
|
├──▶ scripts/find_threads.py, top_twitter_queries.py (no browser, API + DB dedup)
|
|
69
69
|
├──▶ scripts/pick_project.py (weighted project rotation)
|
|
70
70
|
├──▶ scripts/top_performers.py (feedback report from past stats)
|
|
71
|
-
└──▶
|
|
71
|
+
└──▶ Postgres (DATABASE_URL in .env)
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Each `skill/run-*.sh`:
|
|
@@ -104,7 +104,7 @@ launchctl load ~/Library/LaunchAgents/com.m13v.social-twitter-cycle.plist
|
|
|
104
104
|
| `/social-autoposter engage` | Scan and reply to responses on our posts |
|
|
105
105
|
| `/social-autoposter audit` | Full browser audit of all posts |
|
|
106
106
|
|
|
107
|
-
View live stats at `https://s4l.ai/stats/<your-handle>` once posts start landing in
|
|
107
|
+
View live stats at `https://s4l.ai/stats/<your-handle>` once posts start landing in Postgres.
|
|
108
108
|
|
|
109
109
|
## Repo layout
|
|
110
110
|
|
|
@@ -114,7 +114,7 @@ social-autoposter/
|
|
|
114
114
|
├── bin/cli.js installer + dashboard launcher
|
|
115
115
|
├── browser-agent-configs/ Playwright MCP templates (twitter/reddit/linkedin)
|
|
116
116
|
├── config.example.json config template
|
|
117
|
-
├── schema-postgres.sql
|
|
117
|
+
├── schema-postgres.sql Postgres schema
|
|
118
118
|
├── setup/SKILL.md interactive setup wizard skill (locked)
|
|
119
119
|
├── scripts/ Python and JS helpers (no browser, no LLM)
|
|
120
120
|
├── skill/ shell wrappers invoked by launchd
|
package/SKILL.md
CHANGED
|
@@ -41,7 +41,7 @@ Key fields you'll use throughout every workflow:
|
|
|
41
41
|
- `subreddits` — list of subreddits to monitor and post in
|
|
42
42
|
- `content_angle` — the user's unique perspective for writing authentic comments
|
|
43
43
|
- `projects` — products/repos to mention naturally when relevant (each has `name`, `description`, `website`, `github`, `topics`)
|
|
44
|
-
- `database` — unused (DB is
|
|
44
|
+
- `database` — unused (DB is Postgres via `DATABASE_URL` in `.env`)
|
|
45
45
|
|
|
46
46
|
Use these values everywhere below instead of any hardcoded names or links.
|
|
47
47
|
|
|
@@ -132,7 +132,7 @@ Set `engagement_style` to the style you chose for this post (e.g., 'critic', 'st
|
|
|
132
132
|
|
|
133
133
|
Use the account value from `config.json` for `our_account`.
|
|
134
134
|
|
|
135
|
-
Posts are written directly to the
|
|
135
|
+
Posts are written directly to the Postgres database. No separate post-sync step is required.
|
|
136
136
|
|
|
137
137
|
---
|
|
138
138
|
|
|
@@ -229,7 +229,7 @@ Daily-cadence original Reddit threads across all products, automated via launchd
|
|
|
229
229
|
python3 ~/social-autoposter/scripts/update_stats.py
|
|
230
230
|
```
|
|
231
231
|
|
|
232
|
-
After running, view updated stats at `https://s4l.ai/stats/[handle]`. Stats are read from the same
|
|
232
|
+
After running, view updated stats at `https://s4l.ai/stats/[handle]`. Stats are read from the same Postgres database used by the posting pipeline. Changes appear on the website within ~5 minutes.
|
|
233
233
|
|
|
234
234
|
---
|
|
235
235
|
|
package/bin/cli.js
CHANGED
|
@@ -33,7 +33,7 @@ const ENV_TEMPLATE = `# social-autoposter environment variables
|
|
|
33
33
|
# Get it from: https://www.moltbook.com/settings/api
|
|
34
34
|
MOLTBOOK_API_KEY=
|
|
35
35
|
|
|
36
|
-
#
|
|
36
|
+
# Postgres connection string. Bring your own Postgres DB, apply schema with:
|
|
37
37
|
# psql "$DATABASE_URL" -f schema-postgres.sql
|
|
38
38
|
# Format: postgresql://<user>:<password>@<host>/<db>?sslmode=require
|
|
39
39
|
DATABASE_URL=
|
|
@@ -153,20 +153,20 @@ function isAppMakerVm() {
|
|
|
153
153
|
// AppMaker-specific TWITTER_CDP_URL before its `${VAR:-default}` fallback hits.
|
|
154
154
|
// Idempotent: rewrites the file every invocation so a config edit on the VM
|
|
155
155
|
// can't drift away from what cli.js intends.
|
|
156
|
-
function writeAppMakerEnvFile() {
|
|
156
|
+
function writeAppMakerEnvFile(handleFromDb) {
|
|
157
157
|
const envPath = path.join(HOME, '.social-autoposter-env');
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
158
|
+
// Source of truth for the handle is the DB (social_accounts.handle keyed by
|
|
159
|
+
// vm_session_key). bootstrap-vm passes it in. Fallback: preserve a previously
|
|
160
|
+
// set value across rewrites if no DB-sourced handle was provided (matters
|
|
161
|
+
// when this runs from `social-autoposter update` without a fresh DB fetch).
|
|
162
|
+
let preservedHandle = String(handleFromDb || '').trim().replace(/^@/, '');
|
|
163
|
+
if (!preservedHandle) {
|
|
164
|
+
try {
|
|
165
|
+
const prev = fs.readFileSync(envPath, 'utf8');
|
|
166
|
+
const m = prev.match(/^\s*export\s+AUTOPOSTER_TWITTER_HANDLE=(.+)\s*$/m);
|
|
167
|
+
if (m) preservedHandle = m[1].trim();
|
|
168
|
+
} catch { /* no prior file */ }
|
|
169
|
+
}
|
|
170
170
|
|
|
171
171
|
const lines = [
|
|
172
172
|
'# social-autoposter per-host env overrides',
|
|
@@ -290,6 +290,115 @@ function applyAppMakerMcpConfigOverrides() {
|
|
|
290
290
|
// uv installed and broke Phase 1's Claude scan (the MCP server's `command:
|
|
291
291
|
// /root/.local/bin/uv` resolved to ENOENT, Claude got no tools, returned an
|
|
292
292
|
// empty envelope).
|
|
293
|
+
// AppMaker VM self-bootstrap. Single entry point that the appmaker template
|
|
294
|
+
// startup.sh calls on every fresh sandbox boot. Reads the stable sessionKey
|
|
295
|
+
// from /run/mk0r-session.json (which the appmaker bridge rewrites on every
|
|
296
|
+
// session bind, and which survives E2B sandbox substitution — only the
|
|
297
|
+
// sandboxId changes), then asks the social-autoposter HTTP API which Twitter
|
|
298
|
+
// account this VM is bound to (handle + stored login cookies, keyed by
|
|
299
|
+
// social_accounts.vm_session_key). With that single DB answer it sets up
|
|
300
|
+
// everything: env file (with the DB-sourced handle), profile symlink, MCP
|
|
301
|
+
// config (BH_PORT=9222), uuid-runtime, then restores the Twitter login by
|
|
302
|
+
// re-injecting the stored cookies via CDP.
|
|
303
|
+
//
|
|
304
|
+
// This is the "one proper fix" for sandbox substitution: the VM holds no
|
|
305
|
+
// per-VM state on disk — the DB does, keyed by the stable sessionKey. So
|
|
306
|
+
// any fresh sandbox can rebuild itself by reading /run/mk0r-session.json
|
|
307
|
+
// and calling one route.
|
|
308
|
+
function bootstrapVm() {
|
|
309
|
+
if (!isAppMakerVm()) {
|
|
310
|
+
console.error('bootstrap-vm: not an AppMaker VM (no /opt/startup.sh + CDP :9222). Use `init` or `update` on dev boxes.');
|
|
311
|
+
process.exit(2);
|
|
312
|
+
}
|
|
313
|
+
console.log(' AppMaker VM bootstrap: resolving identity from DB by sessionKey...');
|
|
314
|
+
|
|
315
|
+
let sessionKey;
|
|
316
|
+
try {
|
|
317
|
+
const raw = fs.readFileSync('/run/mk0r-session.json', 'utf8');
|
|
318
|
+
sessionKey = (JSON.parse(raw) || {}).sessionKey;
|
|
319
|
+
} catch (err) {
|
|
320
|
+
console.error(`bootstrap-vm: cannot read /run/mk0r-session.json: ${err.message}`);
|
|
321
|
+
process.exit(3);
|
|
322
|
+
}
|
|
323
|
+
if (!sessionKey) {
|
|
324
|
+
console.error('bootstrap-vm: /run/mk0r-session.json has no sessionKey');
|
|
325
|
+
process.exit(3);
|
|
326
|
+
}
|
|
327
|
+
console.log(` sessionKey=${sessionKey}`);
|
|
328
|
+
|
|
329
|
+
// Get the X-Installation header via identity.py (same Python helper http_api.py
|
|
330
|
+
// uses, so auth stays single-sourced).
|
|
331
|
+
const identityPath = path.join(PKG_ROOT, 'scripts', 'identity.py');
|
|
332
|
+
const headerRes = spawnSync('/usr/bin/python3', [identityPath, 'header'], {
|
|
333
|
+
encoding: 'utf8',
|
|
334
|
+
});
|
|
335
|
+
if (headerRes.status !== 0) {
|
|
336
|
+
console.error(`bootstrap-vm: identity.py header failed: ${headerRes.stderr || headerRes.error}`);
|
|
337
|
+
process.exit(4);
|
|
338
|
+
}
|
|
339
|
+
const installHeader = (headerRes.stdout || '').trim();
|
|
340
|
+
|
|
341
|
+
const base = (process.env.AUTOPOSTER_API_BASE || 'https://s4l.ai').replace(/\/+$/, '');
|
|
342
|
+
const url = `${base}/api/v1/twitter/vm-session?session_key=${encodeURIComponent(sessionKey)}`;
|
|
343
|
+
console.log(` GET ${url}`);
|
|
344
|
+
|
|
345
|
+
// Use curl (always present on the appmaker template) so we don't pull in
|
|
346
|
+
// a Node HTTP dep here.
|
|
347
|
+
const curl = spawnSync('curl', [
|
|
348
|
+
'-sS', '--max-time', '15',
|
|
349
|
+
'-H', `X-Installation: ${installHeader}`,
|
|
350
|
+
'-H', 'Content-Type: application/json',
|
|
351
|
+
url,
|
|
352
|
+
], { encoding: 'utf8' });
|
|
353
|
+
if (curl.status !== 0) {
|
|
354
|
+
console.error(`bootstrap-vm: curl failed: ${curl.stderr || curl.error}`);
|
|
355
|
+
process.exit(5);
|
|
356
|
+
}
|
|
357
|
+
let payload;
|
|
358
|
+
try {
|
|
359
|
+
payload = JSON.parse(curl.stdout || '{}');
|
|
360
|
+
} catch (err) {
|
|
361
|
+
console.error(`bootstrap-vm: bad JSON from API: ${curl.stdout.slice(0, 300)}`);
|
|
362
|
+
process.exit(6);
|
|
363
|
+
}
|
|
364
|
+
if (!payload.ok || !payload.data) {
|
|
365
|
+
console.error(`bootstrap-vm: API error: ${JSON.stringify(payload).slice(0, 300)}`);
|
|
366
|
+
process.exit(7);
|
|
367
|
+
}
|
|
368
|
+
const { handle, cookies, vm_project_id } = payload.data;
|
|
369
|
+
if (!handle) {
|
|
370
|
+
console.error('bootstrap-vm: API returned no handle. social_accounts.vm_session_key may be unset for this VM.');
|
|
371
|
+
process.exit(8);
|
|
372
|
+
}
|
|
373
|
+
console.log(` bound to @${handle} (vm_project_id=${vm_project_id || 'none'}, cookies=${(cookies || []).length})`);
|
|
374
|
+
|
|
375
|
+
// Write env file with DB-sourced handle (durable across `social-autoposter update`).
|
|
376
|
+
writeAppMakerEnvFile(handle);
|
|
377
|
+
|
|
378
|
+
// Existing setup steps. installBrowserHarness already installs uuid-runtime,
|
|
379
|
+
// symlinks the profile, and patches the MCP config — call it directly.
|
|
380
|
+
installBrowserHarness();
|
|
381
|
+
|
|
382
|
+
// Restore the Twitter login if we have stored cookies and the Chrome is
|
|
383
|
+
// up. No-op when Chrome isn't reachable yet (startup ordering); the cycle
|
|
384
|
+
// preflight will run restore_twitter_session.py on its next tick.
|
|
385
|
+
if ((cookies || []).length > 0) {
|
|
386
|
+
const restorePath = path.join(HOME, 'social-autoposter', 'scripts', 'restore_twitter_session.py');
|
|
387
|
+
if (fs.existsSync(restorePath)) {
|
|
388
|
+
console.log(' invoking restore_twitter_session.py to re-inject cookies...');
|
|
389
|
+
// Source the env file so AUTOPOSTER_TWITTER_HANDLE / TWITTER_CDP_URL are set.
|
|
390
|
+
const r = spawnSync('bash', ['-lc',
|
|
391
|
+
`source ${HOME}/.social-autoposter-env 2>/dev/null; /usr/bin/python3 ${restorePath} || true`,
|
|
392
|
+
], { stdio: 'inherit' });
|
|
393
|
+
void r;
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
console.log(' no stored cookies; manual login still required this once.');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
console.log(' bootstrap-vm: done.');
|
|
400
|
+
}
|
|
401
|
+
|
|
293
402
|
function installBrowserHarness() {
|
|
294
403
|
const onAppMaker = isAppMakerVm();
|
|
295
404
|
if (onAppMaker) {
|
|
@@ -604,7 +713,7 @@ function init() {
|
|
|
604
713
|
console.log(' 1. Edit ~/social-autoposter/config.json with your accounts');
|
|
605
714
|
console.log(' 2. Tell your Claude agent: "set up social autoposter"');
|
|
606
715
|
console.log(' (uses the setup/SKILL.md wizard for browser login verification)');
|
|
607
|
-
console.log(' 3. Posts are logged to the shared
|
|
716
|
+
console.log(' 3. Posts are logged to the shared Postgres DB (DATABASE_URL in .env)');
|
|
608
717
|
}
|
|
609
718
|
|
|
610
719
|
function update() {
|
|
@@ -745,6 +854,8 @@ if (cmd === 'init') {
|
|
|
745
854
|
init();
|
|
746
855
|
} else if (cmd === 'update') {
|
|
747
856
|
update();
|
|
857
|
+
} else if (cmd === 'bootstrap-vm') {
|
|
858
|
+
bootstrapVm();
|
|
748
859
|
} else if (cmd === 'export-cookies') {
|
|
749
860
|
// Forward to cookie-helper with 'export' + remaining args
|
|
750
861
|
process.argv = [process.argv[0], process.argv[1], 'export', ...process.argv.slice(3)];
|
|
@@ -762,6 +873,7 @@ if (cmd === 'init') {
|
|
|
762
873
|
console.log(' npx social-autoposter open the dashboard');
|
|
763
874
|
console.log(' npx social-autoposter init first-time setup');
|
|
764
875
|
console.log(' npx social-autoposter update update scripts, preserve config');
|
|
876
|
+
console.log(' npx social-autoposter bootstrap-vm AppMaker VM self-bootstrap (DB-driven)');
|
|
765
877
|
console.log(' npx social-autoposter export-cookies [dir] export browser cookies');
|
|
766
878
|
console.log(' npx social-autoposter import-cookies [dir] import browser cookies');
|
|
767
879
|
}
|