social-autoposter 1.2.0 → 1.3.1
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 +7 -8
- package/SKILL.md +22 -4
- package/bin/auth.js +110 -0
- package/bin/cli.js +261 -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 +13813 -1109
- package/package.json +17 -5
- package/requirements.txt +7 -0
- package/schema-postgres.sql +208 -8
- 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/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 +13 -0
- package/scripts/blog_refactor_single_route.py +196 -0
- package/scripts/campaign_bump.py +23 -19
- 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 +1 -1
- 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 +44 -8
- 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 +49 -28
- package/scripts/engage_reddit.py +796 -116
- package/scripts/engagement_styles.py +686 -71
- package/scripts/engagement_styles_extra.json +65 -0
- package/scripts/enrich_twitter_candidates.py +37 -40
- 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 +104 -15
- 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 +191 -26
- package/scripts/linkedin_browser.py +487 -1440
- 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 +162 -2
- 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 +23 -7
- package/scripts/octolens_twitter_cdp.py +20 -7
- package/scripts/pending_threads.py +277 -0
- package/scripts/phase_d_new_comments.py +2 -2
- package/scripts/pick_project.py +27 -10
- package/scripts/pick_thread_target.py +87 -22
- package/scripts/pick_twitter_thread_target.py +245 -0
- package/scripts/poll_web_chat.py +72 -0
- package/scripts/post_github.py +925 -344
- package/scripts/post_reddit.py +1941 -156
- 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 +629 -67
- package/scripts/reddit_browser_lock.py +571 -0
- package/scripts/reddit_chat_sync.py +710 -0
- package/scripts/reddit_tools.py +538 -72
- 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 +81 -46
- 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 +268 -54
- 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 +1060 -129
- package/scripts/twitter_compose_dm.py +1 -1
- 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 -12
- package/skill/engage.sh +63 -216
- package/skill/run-github.sh +73 -10
- package/skill/run-linkedin.sh +818 -69
- package/skill/run-moltbook.sh +22 -70
- 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/backfill_twitter_urls.py +0 -223
- package/scripts/backfill_twitter_urls_v2.py +0 -255
- package/scripts/backfill_twitter_urls_v3.py +0 -345
- package/scripts/diagnose_linkedin_agent.py +0 -196
- package/scripts/find_tweets.py +0 -154
- package/scripts/linkedin_auth_check.py +0 -415
- package/scripts/log_fazm_linkedin.py +0 -62
- package/scripts/log_fazm_linkedin_batch2.py +0 -111
- package/scripts/octolens_twitter_read.py +0 -46
- package/scripts/phase_d_resolve.py +0 -226
- package/scripts/reconcile_reply_urls.py +0 -114
- package/scripts/recover_linkedin_urls.py +0 -301
- package/scripts/run_pieline_linkedin_batch.py +0 -165
- package/scripts/scan_linkedin_notifications.py +0 -273
- package/scripts/scan_replies.py +0 -547
- package/scripts/scan_twitter_mentions.py +0 -175
- package/scripts/scrape_linkedin_stats.py +0 -173
- package/scripts/twitter_api.py +0 -258
- package/skill/run-reddit.sh +0 -111
- package/skill/run-twitter.sh +0 -108
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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
|
-
Posts are written to a
|
|
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
8
|
|
|
@@ -29,7 +29,7 @@ npx social-autoposter init
|
|
|
29
29
|
`bin/cli.js` does all of the wiring in one shot:
|
|
30
30
|
|
|
31
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
|
|
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
33
|
3. Installs `psycopg2-binary` via `pip3` if missing
|
|
34
34
|
4. Generates launchd plists in `~/social-autoposter/launchd/` with the user's actual `HOME` and `PATH`
|
|
35
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`.
|
|
@@ -63,7 +63,7 @@ launchd ──▶ skill/run-{platform}.sh ──▶ claude -p --strict-mcp-
|
|
|
63
63
|
│ │
|
|
64
64
|
│ └──▶ ~/.claude/browser-profiles/{platform}/ (persistent userDataDir)
|
|
65
65
|
│
|
|
66
|
-
├──▶ scripts/
|
|
66
|
+
├──▶ scripts/find_threads.py, top_twitter_queries.py (no browser, API + DB dedup)
|
|
67
67
|
├──▶ scripts/pick_project.py (weighted project rotation)
|
|
68
68
|
├──▶ scripts/top_performers.py (feedback report from past stats)
|
|
69
69
|
└──▶ Neon Postgres (DATABASE_URL in .env)
|
|
@@ -78,19 +78,18 @@ Each `skill/run-*.sh`:
|
|
|
78
78
|
5. Calls `find_*.py` for API-side candidates already deduped against the DB
|
|
79
79
|
6. Spawns a child Claude process with `--strict-mcp-config` so it only sees the one platform's browser MCP
|
|
80
80
|
|
|
81
|
-
The launchd schedules
|
|
81
|
+
The launchd schedules generated by `bin/cli.js` on install:
|
|
82
82
|
|
|
83
83
|
| Job | Cadence |
|
|
84
84
|
|-----|---------|
|
|
85
|
-
| `com.m13v.social-autoposter` (`run.sh`) | every 3600 s (hourly) |
|
|
86
85
|
| `com.m13v.social-stats` (`stats.sh`) | every 21600 s (6 h) |
|
|
87
86
|
| `com.m13v.social-engage` (`engage.sh`) | every 21600 s (6 h) |
|
|
88
87
|
|
|
89
|
-
|
|
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:
|
|
90
89
|
|
|
91
90
|
```bash
|
|
92
|
-
ln -sf ~/social-autoposter/launchd/com.m13v.social-twitter.plist ~/Library/LaunchAgents/
|
|
93
|
-
launchctl load ~/Library/LaunchAgents/com.m13v.social-twitter.plist
|
|
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
|
|
94
93
|
```
|
|
95
94
|
|
|
96
95
|
## Skill commands
|
package/SKILL.md
CHANGED
|
@@ -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
|
|
|
@@ -205,7 +206,7 @@ Daily-cadence original Reddit threads across all products, automated via launchd
|
|
|
205
206
|
- `own_community`: `{subreddit, cadence, floor_days}` (optional). Defaults to 1-day floor.
|
|
206
207
|
- `external_subreddits`: list of external subs (default 3-day floor, override via `external_floor_days`)
|
|
207
208
|
- `topic_angles`: discussion-starter ideas the agent picks from
|
|
208
|
-
-
|
|
209
|
+
- Voice guidance comes from `projects[].voice` (tone, never)
|
|
209
210
|
- `content_sources.guide_dir` / `link_base`: optional source paths/URLs
|
|
210
211
|
- `dynamic_context.day_counter` / `static_facts`: live-calculated facts injected into the prompt
|
|
211
212
|
|
|
@@ -213,7 +214,7 @@ Daily-cadence original Reddit threads across all products, automated via launchd
|
|
|
213
214
|
|
|
214
215
|
**Picker** (`scripts/pick_thread_target.py`): weighted project selection with:
|
|
215
216
|
- Per-sub floor-days filter (queries `posts` table for this account's last original thread)
|
|
216
|
-
- `
|
|
217
|
+
- `subreddit_bans` filter: `banned` (can't post or comment) + `skip_threads` (threads blocked, comments OK)
|
|
217
218
|
- Own-community candidates always picked first when eligible
|
|
218
219
|
|
|
219
220
|
**Schedule**: `com.m13v.social-reddit-threads.plist` fires 4x/day at 00:15, 06:15, 12:15, 18:15.
|
|
@@ -236,7 +237,8 @@ After running, view updated stats at `https://s4l.ai/stats/[handle]`. Stats are
|
|
|
236
237
|
|
|
237
238
|
### Phase A: Scan for replies (no browser)
|
|
238
239
|
```bash
|
|
239
|
-
python3 ~/social-autoposter/scripts/
|
|
240
|
+
python3 ~/social-autoposter/scripts/scan_reddit_replies.py
|
|
241
|
+
python3 ~/social-autoposter/scripts/scan_moltbook_replies.py
|
|
240
242
|
```
|
|
241
243
|
|
|
242
244
|
### Phase B: Respond to pending replies
|
|
@@ -305,6 +307,22 @@ GOOD title: "just did my 7th course, some things that surprised me"
|
|
|
305
307
|
BAD body: Structured with headers, bold, numbered lists, "As a tech founder..."
|
|
306
308
|
GOOD body: Paragraphs, incomplete thoughts, personal details, casual tone, ends with a genuine question
|
|
307
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
|
+
|
|
308
326
|
---
|
|
309
327
|
|
|
310
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
|
+
};
|
package/bin/cli.js
CHANGED
|
@@ -6,6 +6,9 @@ const fs = require('fs');
|
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const { spawnSync } = require('child_process');
|
|
8
8
|
|
|
9
|
+
const platform = require('./platform');
|
|
10
|
+
const scheduler = require('./scheduler');
|
|
11
|
+
|
|
9
12
|
const DEST = path.join(os.homedir(), 'social-autoposter');
|
|
10
13
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
11
14
|
const HOME = os.homedir();
|
|
@@ -15,13 +18,26 @@ const COPY_TARGETS = [
|
|
|
15
18
|
'scripts',
|
|
16
19
|
'schema-postgres.sql',
|
|
17
20
|
'config.example.json',
|
|
18
|
-
'.
|
|
21
|
+
'requirements.txt',
|
|
19
22
|
'SKILL.md',
|
|
20
23
|
'skill',
|
|
21
24
|
'setup',
|
|
22
25
|
'browser-agent-configs',
|
|
23
26
|
];
|
|
24
27
|
|
|
28
|
+
const ENV_TEMPLATE = `# social-autoposter environment variables
|
|
29
|
+
# Fill in your values below.
|
|
30
|
+
|
|
31
|
+
# Moltbook API key (required for Moltbook posting/scanning)
|
|
32
|
+
# Get it from: https://www.moltbook.com/settings/api
|
|
33
|
+
MOLTBOOK_API_KEY=
|
|
34
|
+
|
|
35
|
+
# Neon Postgres connection string. Bring your own Neon DB — apply schema with:
|
|
36
|
+
# psql "$DATABASE_URL" -f schema-postgres.sql
|
|
37
|
+
# Format: postgresql://<user>:<password>@<host>/<db>?sslmode=require
|
|
38
|
+
DATABASE_URL=
|
|
39
|
+
`;
|
|
40
|
+
|
|
25
41
|
// Never overwrite these user files during update
|
|
26
42
|
const USER_FILES = new Set(['config.json', '.env', 'SKILL.md']);
|
|
27
43
|
|
|
@@ -89,13 +105,64 @@ function installBrowserAgentConfigs() {
|
|
|
89
105
|
console.log(` browser profile dirs ready -> ${profilesDir}/{${BROWSER_PROFILES.join(',')}}`);
|
|
90
106
|
}
|
|
91
107
|
|
|
108
|
+
// Register the three browser-agent MCP servers with Claude so they show up
|
|
109
|
+
// under user scope (writes to ~/.claude.json). Idempotent: parses the output
|
|
110
|
+
// of `claude mcp list` and only calls `add-json` for missing entries.
|
|
111
|
+
// If the `claude` CLI is not on PATH, prints manual instructions and returns.
|
|
112
|
+
function registerBrowserAgentMcpServers() {
|
|
113
|
+
const configDir = path.join(HOME, '.claude', 'browser-agent-configs');
|
|
114
|
+
const servers = [
|
|
115
|
+
{ name: 'twitter-agent', file: path.join(configDir, 'twitter-agent-mcp.json') },
|
|
116
|
+
{ name: 'reddit-agent', file: path.join(configDir, 'reddit-agent-mcp.json') },
|
|
117
|
+
{ name: 'linkedin-agent', file: path.join(configDir, 'linkedin-agent-mcp.json') },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const claudeBin = spawnSync('claude', ['--version'], { stdio: 'pipe' });
|
|
121
|
+
if (claudeBin.status !== 0) {
|
|
122
|
+
console.log(' claude CLI not on PATH; skipping MCP registration.');
|
|
123
|
+
console.log(' Once Claude Code is installed, register manually with:');
|
|
124
|
+
for (const s of servers) {
|
|
125
|
+
console.log(` claude mcp add-json ${s.name} "$(jq -c .mcpServers['\\"'${s.name}'\\"'] ${s.file})"`);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const list = spawnSync('claude', ['mcp', 'list'], { encoding: 'utf8' });
|
|
131
|
+
const existing = list.status === 0 ? list.stdout : '';
|
|
132
|
+
|
|
133
|
+
let added = 0;
|
|
134
|
+
let skipped = 0;
|
|
135
|
+
for (const s of servers) {
|
|
136
|
+
if (!fs.existsSync(s.file)) {
|
|
137
|
+
console.warn(` MCP config missing: ${s.file}`);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// `claude mcp list` prints one server per line starting with the name.
|
|
141
|
+
// Use a word-boundary check so twitter-agent does not false-match reddit-agent.
|
|
142
|
+
const re = new RegExp(`(^|\\s)${s.name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}(:|\\s|$)`, 'm');
|
|
143
|
+
if (re.test(existing)) {
|
|
144
|
+
skipped++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const tpl = JSON.parse(fs.readFileSync(s.file, 'utf8'));
|
|
148
|
+
const stanza = tpl.mcpServers && tpl.mcpServers[s.name];
|
|
149
|
+
if (!stanza) {
|
|
150
|
+
console.warn(` ${s.file} has no mcpServers.${s.name} stanza; skipping`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const r = spawnSync('claude', ['mcp', 'add-json', s.name, JSON.stringify(stanza)], { stdio: 'pipe', encoding: 'utf8' });
|
|
154
|
+
if (r.status === 0) {
|
|
155
|
+
added++;
|
|
156
|
+
} else {
|
|
157
|
+
console.warn(` claude mcp add-json ${s.name} failed: ${(r.stderr || r.stdout || '').trim()}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
console.log(` MCP servers registered with Claude (added ${added}, already present ${skipped})`);
|
|
161
|
+
}
|
|
162
|
+
|
|
92
163
|
function generatePlists() {
|
|
93
|
-
// Detect PATH for launchd (include node, homebrew, system)
|
|
94
164
|
const nodeBin = path.dirname(process.execPath);
|
|
95
|
-
const
|
|
96
|
-
const launchdPath = [...pathDirs].join(':');
|
|
97
|
-
|
|
98
|
-
const plists = [
|
|
165
|
+
const jobs = [
|
|
99
166
|
{
|
|
100
167
|
file: 'com.m13v.social-stats.plist',
|
|
101
168
|
label: 'com.m13v.social-stats',
|
|
@@ -105,53 +172,92 @@ function generatePlists() {
|
|
|
105
172
|
stdoutLog: `${DEST}/skill/logs/launchd-stats-stdout.log`,
|
|
106
173
|
stderrLog: `${DEST}/skill/logs/launchd-stats-stderr.log`,
|
|
107
174
|
},
|
|
108
|
-
{
|
|
109
|
-
file: 'com.m13v.social-engage.plist',
|
|
110
|
-
label: 'com.m13v.social-engage',
|
|
111
|
-
script: `${DEST}/skill/engage.sh`,
|
|
112
|
-
interval: 21600,
|
|
113
|
-
runAtLoad: false,
|
|
114
|
-
stdoutLog: `${DEST}/skill/logs/launchd-engage-stdout.log`,
|
|
115
|
-
stderrLog: `${DEST}/skill/logs/launchd-engage-stderr.log`,
|
|
116
|
-
},
|
|
117
175
|
];
|
|
118
176
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
177
|
+
const driver = scheduler.driverFor();
|
|
178
|
+
const env = driver.defaultEnv({ home: HOME, nodeBin });
|
|
179
|
+
const kind = platform.scheduler();
|
|
180
|
+
const outDir = path.join(DEST, kind === 'systemd' ? 'systemd' : 'launchd');
|
|
181
|
+
driver.generate({ jobs, outDir, env });
|
|
182
|
+
console.log(` generated ${kind} units at ${outDir}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// On Linux we translate every shipped launchd plist into a systemd
|
|
186
|
+
// .service + .timer pair at install time. Plists remain the source of truth
|
|
187
|
+
// so the macOS pipeline is untouched; the systemd/ dir is derived.
|
|
188
|
+
function generateSystemdFromPlists() {
|
|
189
|
+
const launchdDriver = scheduler.driverFor('launchd');
|
|
190
|
+
const systemdDriver = scheduler.driverFor('systemd');
|
|
191
|
+
const srcDir = path.join(DEST, 'launchd');
|
|
192
|
+
const outDir = path.join(DEST, 'systemd');
|
|
193
|
+
if (!fs.existsSync(srcDir)) return 0;
|
|
194
|
+
const plists = fs.readdirSync(srcDir).filter(f => f.endsWith('.plist'));
|
|
195
|
+
const nodeBin = path.dirname(process.execPath);
|
|
196
|
+
const env = systemdDriver.defaultEnv({ home: HOME, nodeBin });
|
|
197
|
+
|
|
198
|
+
const jobs = [];
|
|
199
|
+
let skipped = 0;
|
|
200
|
+
for (const f of plists) {
|
|
201
|
+
const xml = fs.readFileSync(path.join(srcDir, f), 'utf8');
|
|
202
|
+
const { label, scriptPath } = launchdDriver.parseUnit(xml);
|
|
203
|
+
if (!label || !scriptPath) { skipped++; continue; }
|
|
204
|
+
const sched = launchdDriver.scheduleFromUnit(xml);
|
|
205
|
+
if (!sched.intervalSecs) {
|
|
206
|
+
console.log(` skip ${f}: calendar schedule not yet translated to OnCalendar`);
|
|
207
|
+
skipped++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
// Plists ship with the publisher's absolute paths baked in. Rebuild
|
|
211
|
+
// paths against the current user's DEST so any user on any host gets
|
|
212
|
+
// correct units without us having to re-ship plists per install target.
|
|
213
|
+
const scriptBase = path.basename(scriptPath);
|
|
214
|
+
const stdoutMatch = (xml.match(/<key>StandardOutPath<\/key>\s*<string>([^<]+)<\/string>/) || [])[1];
|
|
215
|
+
const stderrMatch = (xml.match(/<key>StandardErrorPath<\/key>\s*<string>([^<]+)<\/string>/) || [])[1];
|
|
216
|
+
const shortLabel = label.replace(/^com\.m13v\.social-/, '');
|
|
217
|
+
const stdout = `${DEST}/skill/logs/${stdoutMatch ? path.basename(stdoutMatch) : `launchd-${shortLabel}-stdout.log`}`;
|
|
218
|
+
const stderr = `${DEST}/skill/logs/${stderrMatch ? path.basename(stderrMatch) : `launchd-${shortLabel}-stderr.log`}`;
|
|
219
|
+
const runAtLoad = /<key>RunAtLoad<\/key>\s*<true\s*\/>/.test(xml);
|
|
220
|
+
jobs.push({
|
|
221
|
+
file: f,
|
|
222
|
+
label,
|
|
223
|
+
script: `${DEST}/skill/${scriptBase}`,
|
|
224
|
+
interval: sched.intervalSecs,
|
|
225
|
+
runAtLoad,
|
|
226
|
+
stdoutLog: stdout,
|
|
227
|
+
stderrLog: stderr,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
systemdDriver.generate({ jobs, outDir, env });
|
|
231
|
+
console.log(` translated ${jobs.length} launchd plists -> systemd units (skipped ${skipped})`);
|
|
232
|
+
return jobs.length;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Link every DEST/systemd/*.{service,timer} into ~/.config/systemd/user/ and
|
|
236
|
+
// reload the user daemon. Caller is expected to `systemctl --user enable --now
|
|
237
|
+
// <timer>` for each timer they actually want running; this mirrors how macOS
|
|
238
|
+
// setup leaves loading to the user via the SKILL.md wizard.
|
|
239
|
+
function installSystemdUnits() {
|
|
240
|
+
const driver = scheduler.driverFor('systemd');
|
|
241
|
+
const unitDir = path.join(DEST, 'systemd');
|
|
242
|
+
const agentsDir = platform.agentsDir();
|
|
243
|
+
if (!fs.existsSync(unitDir)) return;
|
|
244
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
245
|
+
const services = fs.readdirSync(unitDir).filter(f => f.endsWith('.service'));
|
|
246
|
+
let linked = 0;
|
|
247
|
+
for (const f of services) {
|
|
248
|
+
if (driver.install(path.join(unitDir, f), agentsDir)) linked++;
|
|
249
|
+
}
|
|
250
|
+
const r = spawnSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf8' });
|
|
251
|
+
if (r.status === 0) {
|
|
252
|
+
console.log(` linked ${linked} unit pair(s) into ${agentsDir}; systemctl --user daemon-reload OK`);
|
|
253
|
+
} else {
|
|
254
|
+
console.warn(` linked ${linked} unit pair(s); daemon-reload failed: ${(r.stderr || '').trim()}`);
|
|
255
|
+
}
|
|
256
|
+
const linger = spawnSync('loginctl', ['show-user', os.userInfo().username, '--property=Linger'], { encoding: 'utf8' });
|
|
257
|
+
if (!/Linger=yes/.test(linger.stdout || '')) {
|
|
258
|
+
console.log(' note: run `sudo loginctl enable-linger $USER` so timers fire when nobody is logged in');
|
|
153
259
|
}
|
|
154
|
-
console.log('
|
|
260
|
+
console.log(' next: systemctl --user enable --now <timer> for each job you want scheduled');
|
|
155
261
|
}
|
|
156
262
|
|
|
157
263
|
function init() {
|
|
@@ -175,8 +281,17 @@ function init() {
|
|
|
175
281
|
// Generate launchd plists with user's actual HOME
|
|
176
282
|
generatePlists();
|
|
177
283
|
|
|
284
|
+
// On Linux, derive systemd units from every plist and link them into
|
|
285
|
+
// ~/.config/systemd/user/. macOS install is unchanged.
|
|
286
|
+
if (platform.scheduler() === 'systemd') {
|
|
287
|
+
generateSystemdFromPlists();
|
|
288
|
+
installSystemdUnits();
|
|
289
|
+
}
|
|
290
|
+
|
|
178
291
|
// Install browser agent MCP configs + profile dirs (skips existing files)
|
|
179
292
|
installBrowserAgentConfigs();
|
|
293
|
+
// Register those MCP servers with Claude so they show up in `claude mcp list`.
|
|
294
|
+
registerBrowserAgentMcpServers();
|
|
180
295
|
|
|
181
296
|
// config.json — only if it doesn't exist
|
|
182
297
|
const configDest = path.join(DEST, 'config.json');
|
|
@@ -187,29 +302,18 @@ function init() {
|
|
|
187
302
|
console.log(' config.json exists — skipping');
|
|
188
303
|
}
|
|
189
304
|
|
|
190
|
-
// .env — only if it doesn't exist
|
|
305
|
+
// .env — only if it doesn't exist. Written from an in-package template so
|
|
306
|
+
// the NPM tarball no longer ships a credential-bearing .env.example file.
|
|
191
307
|
const envDest = path.join(DEST, '.env');
|
|
192
308
|
if (!fs.existsSync(envDest)) {
|
|
193
|
-
fs.
|
|
194
|
-
console.log(' created .env from template');
|
|
309
|
+
fs.writeFileSync(envDest, ENV_TEMPLATE);
|
|
310
|
+
console.log(' created .env from template (fill in DATABASE_URL and MOLTBOOK_API_KEY)');
|
|
195
311
|
} else {
|
|
196
312
|
console.log(' .env exists — skipping');
|
|
197
313
|
}
|
|
198
314
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (pip3Check.status !== 0) {
|
|
202
|
-
console.log(' installing psycopg2-binary (required for Neon DB)...');
|
|
203
|
-
const pipInstall = spawnSync('pip3', ['install', 'psycopg2-binary', '-q'], { stdio: 'inherit' });
|
|
204
|
-
if (pipInstall.status !== 0) {
|
|
205
|
-
console.warn(' WARNING: psycopg2-binary install failed — run manually:');
|
|
206
|
-
console.warn(' pip3 install psycopg2-binary');
|
|
207
|
-
} else {
|
|
208
|
-
console.log(' psycopg2-binary installed');
|
|
209
|
-
}
|
|
210
|
-
} else {
|
|
211
|
-
console.log(' psycopg2-binary already installed');
|
|
212
|
-
}
|
|
315
|
+
installPythonDeps();
|
|
316
|
+
installEngagementStylesSidecar();
|
|
213
317
|
|
|
214
318
|
// Remove stale skill/SKILL.md if it exists (SKILL.md lives at repo root only)
|
|
215
319
|
const skillMd = path.join(DEST, 'skill', 'SKILL.md');
|
|
@@ -259,8 +363,21 @@ function update() {
|
|
|
259
363
|
// Regenerate launchd plists with correct paths
|
|
260
364
|
generatePlists();
|
|
261
365
|
|
|
366
|
+
// Refresh systemd units on Linux so plist changes propagate.
|
|
367
|
+
if (platform.scheduler() === 'systemd') {
|
|
368
|
+
generateSystemdFromPlists();
|
|
369
|
+
installSystemdUnits();
|
|
370
|
+
}
|
|
371
|
+
|
|
262
372
|
// Top up browser agent configs (won't overwrite user customizations)
|
|
263
373
|
installBrowserAgentConfigs();
|
|
374
|
+
// Register any newly added MCP servers with Claude (idempotent).
|
|
375
|
+
registerBrowserAgentMcpServers();
|
|
376
|
+
|
|
377
|
+
// Refresh Python deps every update so version-bumps land on existing installs
|
|
378
|
+
// and the candidate-style sidecar gets merged (preserves VM-side candidates).
|
|
379
|
+
installPythonDeps();
|
|
380
|
+
installEngagementStylesSidecar();
|
|
264
381
|
|
|
265
382
|
// Remove stale skill/SKILL.md if it exists (SKILL.md lives at repo root only)
|
|
266
383
|
const skillMd = path.join(DEST, 'skill', 'SKILL.md');
|
|
@@ -281,18 +398,92 @@ function update() {
|
|
|
281
398
|
console.log('Update complete. config.json was preserved.');
|
|
282
399
|
}
|
|
283
400
|
|
|
401
|
+
// Install Python deps from requirements.txt (preferred) or fall back to the
|
|
402
|
+
// hardcoded list. Idempotent — pip3 install is a no-op when the package is
|
|
403
|
+
// already at the requested version. Playwright also needs the Chromium
|
|
404
|
+
// browser binary; we run `playwright install chromium` after the pip install.
|
|
405
|
+
function installPythonDeps() {
|
|
406
|
+
const reqPath = path.join(PKG_ROOT, 'requirements.txt');
|
|
407
|
+
const base = fs.existsSync(reqPath)
|
|
408
|
+
? ['install', '-r', reqPath, '-q']
|
|
409
|
+
: ['install', '-q', 'psycopg2-binary', 'playwright'];
|
|
410
|
+
console.log(' installing Python deps (psycopg2-binary, playwright, ...)');
|
|
411
|
+
// Debian/Ubuntu 23+ ship a PEP 668 marker that blocks pip3 against the
|
|
412
|
+
// system Python without --break-system-packages. Try without first
|
|
413
|
+
// (safer on macOS) and retry with the flag if the marker fires.
|
|
414
|
+
let r = spawnSync('pip3', base, { stdio: 'inherit' });
|
|
415
|
+
if (r.status !== 0) {
|
|
416
|
+
console.log(' retrying with --break-system-packages (PEP 668 environments)');
|
|
417
|
+
r = spawnSync('pip3', [...base, '--break-system-packages'], { stdio: 'inherit' });
|
|
418
|
+
}
|
|
419
|
+
if (r.status !== 0) {
|
|
420
|
+
console.warn(' WARNING: pip3 install failed — run manually:');
|
|
421
|
+
console.warn(` pip3 ${base.join(' ')} --break-system-packages`);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
// Playwright needs its browser binary downloaded separately. Chromium
|
|
425
|
+
// is the only engine the repo uses today; skip Firefox/WebKit.
|
|
426
|
+
console.log(' installing Playwright Chromium binary (one-time, ~150MB)...');
|
|
427
|
+
const pw = spawnSync('python3', ['-m', 'playwright', 'install', 'chromium'], { stdio: 'inherit' });
|
|
428
|
+
if (pw.status !== 0) {
|
|
429
|
+
console.warn(' WARNING: playwright install chromium failed — run manually:');
|
|
430
|
+
console.warn(' python3 -m playwright install chromium');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Copy the candidate-style sidecar JSON into ~/social-autoposter/scripts/
|
|
435
|
+
// if missing; merge if present so VM-side invented candidates survive
|
|
436
|
+
// across updates. Promoted (status=active) entries from the shipped baseline
|
|
437
|
+
// always win.
|
|
438
|
+
function installEngagementStylesSidecar() {
|
|
439
|
+
const src = path.join(PKG_ROOT, 'scripts', 'engagement_styles_extra.json');
|
|
440
|
+
const dest = path.join(DEST, 'scripts', 'engagement_styles_extra.json');
|
|
441
|
+
if (!fs.existsSync(src)) return;
|
|
442
|
+
|
|
443
|
+
if (!fs.existsSync(dest)) {
|
|
444
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
445
|
+
fs.copyFileSync(src, dest);
|
|
446
|
+
console.log(' installed scripts/engagement_styles_extra.json');
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let shipped = {};
|
|
451
|
+
let local = {};
|
|
452
|
+
try { shipped = JSON.parse(fs.readFileSync(src, 'utf8')) || {}; } catch {}
|
|
453
|
+
try { local = JSON.parse(fs.readFileSync(dest, 'utf8')) || {}; } catch {}
|
|
454
|
+
|
|
455
|
+
// Start from local (preserves VM-only candidates), overlay shipped active
|
|
456
|
+
// entries so newly promoted styles always land. Shipped wins on conflict.
|
|
457
|
+
const merged = { ...local };
|
|
458
|
+
for (const [name, entry] of Object.entries(shipped)) {
|
|
459
|
+
merged[name] = entry;
|
|
460
|
+
}
|
|
461
|
+
fs.writeFileSync(dest, JSON.stringify(merged, null, 2) + '\n');
|
|
462
|
+
console.log(' merged scripts/engagement_styles_extra.json (shipped wins on conflict)');
|
|
463
|
+
}
|
|
464
|
+
|
|
284
465
|
const cmd = process.argv[2];
|
|
285
466
|
if (cmd === 'init') {
|
|
286
467
|
init();
|
|
287
468
|
} else if (cmd === 'update') {
|
|
288
469
|
update();
|
|
470
|
+
} else if (cmd === 'export-cookies') {
|
|
471
|
+
// Forward to cookie-helper with 'export' + remaining args
|
|
472
|
+
process.argv = [process.argv[0], process.argv[1], 'export', ...process.argv.slice(3)];
|
|
473
|
+
require('./cookie-helper.js');
|
|
474
|
+
} else if (cmd === 'import-cookies') {
|
|
475
|
+
// Forward to cookie-helper with 'import' + remaining args
|
|
476
|
+
process.argv = [process.argv[0], process.argv[1], 'import', ...process.argv.slice(3)];
|
|
477
|
+
require('./cookie-helper.js');
|
|
289
478
|
} else if (!cmd) {
|
|
290
479
|
require('./server.js');
|
|
291
480
|
} else {
|
|
292
481
|
console.log('social-autoposter — automated social posting for Claude agents');
|
|
293
482
|
console.log('');
|
|
294
483
|
console.log('Usage:');
|
|
295
|
-
console.log(' npx social-autoposter
|
|
296
|
-
console.log(' npx social-autoposter init
|
|
297
|
-
console.log(' npx social-autoposter update
|
|
484
|
+
console.log(' npx social-autoposter open the dashboard');
|
|
485
|
+
console.log(' npx social-autoposter init first-time setup');
|
|
486
|
+
console.log(' npx social-autoposter update update scripts, preserve config');
|
|
487
|
+
console.log(' npx social-autoposter export-cookies [dir] export browser cookies');
|
|
488
|
+
console.log(' npx social-autoposter import-cookies [dir] import browser cookies');
|
|
298
489
|
}
|