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.
Files changed (47) hide show
  1. package/README.md +6 -6
  2. package/SKILL.md +3 -3
  3. package/bin/cli.js +127 -15
  4. package/bin/server.js +329 -10
  5. package/package.json +1 -1
  6. package/requirements.txt +4 -0
  7. package/schema-postgres.sql +52 -1
  8. package/scripts/active_campaigns.py +3 -3
  9. package/scripts/batch_send_dms.py +5 -1
  10. package/scripts/campaign_bump.py +3 -3
  11. package/scripts/check_unread_web_chats.py +2 -2
  12. package/scripts/db.py +7 -6
  13. package/scripts/dm_short_links.py +4 -4
  14. package/scripts/engagement_styles.py +173 -2
  15. package/scripts/generate_daily_human_style.py +314 -0
  16. package/scripts/get_run_cost.py +2 -2
  17. package/scripts/heartbeat.sh +1 -1
  18. package/scripts/ingest_web_chat_replies.py +1 -1
  19. package/scripts/link_tail.py +2 -5
  20. package/scripts/log_claude_session.py +5 -5
  21. package/scripts/log_post.py +11 -0
  22. package/scripts/phase_d_edit.py +5 -1
  23. package/scripts/phase_d_new_comments.py +3 -1
  24. package/scripts/pick_project.py +6 -6
  25. package/scripts/poll_web_chat.py +1 -1
  26. package/scripts/precompute_dashboard_stats.py +4 -4
  27. package/scripts/project_stats_json.py +1 -1
  28. package/scripts/realign_sequences.py +60 -0
  29. package/scripts/score_linkedin_candidates.py +7 -1
  30. package/scripts/scratch_seo_gsc.py +134 -0
  31. package/scripts/scratch_seo_posthog.py +179 -0
  32. package/scripts/scratch_seo_volume.py +136 -0
  33. package/scripts/send_batch_dms.sh +4 -1
  34. package/scripts/top_performers.py +5 -5
  35. package/scripts/twitter_browser.py +47 -3
  36. package/scripts/twitter_post_plan.py +86 -0
  37. package/setup/SKILL.md +5 -5
  38. package/skill/check-web-chats.sh +7 -7
  39. package/skill/github-engage.sh +1 -1
  40. package/skill/link-edit-github.sh +26 -6
  41. package/skill/link-edit-linkedin.sh +26 -6
  42. package/skill/run-generate-daily-style.sh +45 -0
  43. package/skill/run-instagram-render.sh +2 -2
  44. package/skill/run-linkedin.sh +1 -1
  45. package/skill/run-reddit-search.sh +2 -2
  46. package/skill/run-twitter-cycle-singleton.sh +66 -0
  47. 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 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.
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 Neon directly)
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 Neon connection
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
- └──▶ Neon Postgres (DATABASE_URL in .env)
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 Neon.
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 Neon schema
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 Neon Postgres via `DATABASE_URL` in `.env`)
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 Neon Postgres database. No separate post-sync step is required.
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 Neon Postgres database used by the posting pipeline. Changes appear on the website within ~5 minutes.
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
- # Neon Postgres connection string. Bring your own Neon DB apply schema with:
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
- // Preserve a previously-set AUTOPOSTER_TWITTER_HANDLE across rewrites. This
159
- // is per-VM state (which @handle this sandbox posts as) and is NOT something
160
- // the generic bootstrap knows the operator sets it once after login, and
161
- // the cycle + restore_twitter_session.py read it via twitter_account.resolve_handle().
162
- // The sandbox's config.json (which also carries the handle) gets reseeded
163
- // from /etc/skel-root on substitution, so the env var is the durable home.
164
- let preservedHandle = '';
165
- try {
166
- const prev = fs.readFileSync(envPath, 'utf8');
167
- const m = prev.match(/^\s*export\s+AUTOPOSTER_TWITTER_HANDLE=(.+)\s*$/m);
168
- if (m) preservedHandle = m[1].trim();
169
- } catch { /* no prior file */ }
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 Neon DB (DATABASE_URL in .env)');
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
  }