social-autoposter 1.2.0 → 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 +7 -8
- package/SKILL.md +22 -4
- package/bin/auth.js +110 -0
- package/bin/cli.js +253 -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,25 @@ const COPY_TARGETS = [
|
|
|
15
18
|
'scripts',
|
|
16
19
|
'schema-postgres.sql',
|
|
17
20
|
'config.example.json',
|
|
18
|
-
'.env.example',
|
|
19
21
|
'SKILL.md',
|
|
20
22
|
'skill',
|
|
21
23
|
'setup',
|
|
22
24
|
'browser-agent-configs',
|
|
23
25
|
];
|
|
24
26
|
|
|
27
|
+
const ENV_TEMPLATE = `# social-autoposter environment variables
|
|
28
|
+
# Fill in your values below.
|
|
29
|
+
|
|
30
|
+
# Moltbook API key (required for Moltbook posting/scanning)
|
|
31
|
+
# Get it from: https://www.moltbook.com/settings/api
|
|
32
|
+
MOLTBOOK_API_KEY=
|
|
33
|
+
|
|
34
|
+
# Neon Postgres connection string. Bring your own Neon DB — apply schema with:
|
|
35
|
+
# psql "$DATABASE_URL" -f schema-postgres.sql
|
|
36
|
+
# Format: postgresql://<user>:<password>@<host>/<db>?sslmode=require
|
|
37
|
+
DATABASE_URL=
|
|
38
|
+
`;
|
|
39
|
+
|
|
25
40
|
// Never overwrite these user files during update
|
|
26
41
|
const USER_FILES = new Set(['config.json', '.env', 'SKILL.md']);
|
|
27
42
|
|
|
@@ -89,13 +104,64 @@ function installBrowserAgentConfigs() {
|
|
|
89
104
|
console.log(` browser profile dirs ready -> ${profilesDir}/{${BROWSER_PROFILES.join(',')}}`);
|
|
90
105
|
}
|
|
91
106
|
|
|
107
|
+
// Register the three browser-agent MCP servers with Claude so they show up
|
|
108
|
+
// under user scope (writes to ~/.claude.json). Idempotent: parses the output
|
|
109
|
+
// of `claude mcp list` and only calls `add-json` for missing entries.
|
|
110
|
+
// If the `claude` CLI is not on PATH, prints manual instructions and returns.
|
|
111
|
+
function registerBrowserAgentMcpServers() {
|
|
112
|
+
const configDir = path.join(HOME, '.claude', 'browser-agent-configs');
|
|
113
|
+
const servers = [
|
|
114
|
+
{ name: 'twitter-agent', file: path.join(configDir, 'twitter-agent-mcp.json') },
|
|
115
|
+
{ name: 'reddit-agent', file: path.join(configDir, 'reddit-agent-mcp.json') },
|
|
116
|
+
{ name: 'linkedin-agent', file: path.join(configDir, 'linkedin-agent-mcp.json') },
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const claudeBin = spawnSync('claude', ['--version'], { stdio: 'pipe' });
|
|
120
|
+
if (claudeBin.status !== 0) {
|
|
121
|
+
console.log(' claude CLI not on PATH; skipping MCP registration.');
|
|
122
|
+
console.log(' Once Claude Code is installed, register manually with:');
|
|
123
|
+
for (const s of servers) {
|
|
124
|
+
console.log(` claude mcp add-json ${s.name} "$(jq -c .mcpServers['\\"'${s.name}'\\"'] ${s.file})"`);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const list = spawnSync('claude', ['mcp', 'list'], { encoding: 'utf8' });
|
|
130
|
+
const existing = list.status === 0 ? list.stdout : '';
|
|
131
|
+
|
|
132
|
+
let added = 0;
|
|
133
|
+
let skipped = 0;
|
|
134
|
+
for (const s of servers) {
|
|
135
|
+
if (!fs.existsSync(s.file)) {
|
|
136
|
+
console.warn(` MCP config missing: ${s.file}`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
// `claude mcp list` prints one server per line starting with the name.
|
|
140
|
+
// Use a word-boundary check so twitter-agent does not false-match reddit-agent.
|
|
141
|
+
const re = new RegExp(`(^|\\s)${s.name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}(:|\\s|$)`, 'm');
|
|
142
|
+
if (re.test(existing)) {
|
|
143
|
+
skipped++;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const tpl = JSON.parse(fs.readFileSync(s.file, 'utf8'));
|
|
147
|
+
const stanza = tpl.mcpServers && tpl.mcpServers[s.name];
|
|
148
|
+
if (!stanza) {
|
|
149
|
+
console.warn(` ${s.file} has no mcpServers.${s.name} stanza; skipping`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const r = spawnSync('claude', ['mcp', 'add-json', s.name, JSON.stringify(stanza)], { stdio: 'pipe', encoding: 'utf8' });
|
|
153
|
+
if (r.status === 0) {
|
|
154
|
+
added++;
|
|
155
|
+
} else {
|
|
156
|
+
console.warn(` claude mcp add-json ${s.name} failed: ${(r.stderr || r.stdout || '').trim()}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
console.log(` MCP servers registered with Claude (added ${added}, already present ${skipped})`);
|
|
160
|
+
}
|
|
161
|
+
|
|
92
162
|
function generatePlists() {
|
|
93
|
-
// Detect PATH for launchd (include node, homebrew, system)
|
|
94
163
|
const nodeBin = path.dirname(process.execPath);
|
|
95
|
-
const
|
|
96
|
-
const launchdPath = [...pathDirs].join(':');
|
|
97
|
-
|
|
98
|
-
const plists = [
|
|
164
|
+
const jobs = [
|
|
99
165
|
{
|
|
100
166
|
file: 'com.m13v.social-stats.plist',
|
|
101
167
|
label: 'com.m13v.social-stats',
|
|
@@ -105,53 +171,92 @@ function generatePlists() {
|
|
|
105
171
|
stdoutLog: `${DEST}/skill/logs/launchd-stats-stdout.log`,
|
|
106
172
|
stderrLog: `${DEST}/skill/logs/launchd-stats-stderr.log`,
|
|
107
173
|
},
|
|
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
174
|
];
|
|
118
175
|
|
|
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
|
-
|
|
176
|
+
const driver = scheduler.driverFor();
|
|
177
|
+
const env = driver.defaultEnv({ home: HOME, nodeBin });
|
|
178
|
+
const kind = platform.scheduler();
|
|
179
|
+
const outDir = path.join(DEST, kind === 'systemd' ? 'systemd' : 'launchd');
|
|
180
|
+
driver.generate({ jobs, outDir, env });
|
|
181
|
+
console.log(` generated ${kind} units at ${outDir}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// On Linux we translate every shipped launchd plist into a systemd
|
|
185
|
+
// .service + .timer pair at install time. Plists remain the source of truth
|
|
186
|
+
// so the macOS pipeline is untouched; the systemd/ dir is derived.
|
|
187
|
+
function generateSystemdFromPlists() {
|
|
188
|
+
const launchdDriver = scheduler.driverFor('launchd');
|
|
189
|
+
const systemdDriver = scheduler.driverFor('systemd');
|
|
190
|
+
const srcDir = path.join(DEST, 'launchd');
|
|
191
|
+
const outDir = path.join(DEST, 'systemd');
|
|
192
|
+
if (!fs.existsSync(srcDir)) return 0;
|
|
193
|
+
const plists = fs.readdirSync(srcDir).filter(f => f.endsWith('.plist'));
|
|
194
|
+
const nodeBin = path.dirname(process.execPath);
|
|
195
|
+
const env = systemdDriver.defaultEnv({ home: HOME, nodeBin });
|
|
196
|
+
|
|
197
|
+
const jobs = [];
|
|
198
|
+
let skipped = 0;
|
|
199
|
+
for (const f of plists) {
|
|
200
|
+
const xml = fs.readFileSync(path.join(srcDir, f), 'utf8');
|
|
201
|
+
const { label, scriptPath } = launchdDriver.parseUnit(xml);
|
|
202
|
+
if (!label || !scriptPath) { skipped++; continue; }
|
|
203
|
+
const sched = launchdDriver.scheduleFromUnit(xml);
|
|
204
|
+
if (!sched.intervalSecs) {
|
|
205
|
+
console.log(` skip ${f}: calendar schedule not yet translated to OnCalendar`);
|
|
206
|
+
skipped++;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
// Plists ship with the publisher's absolute paths baked in. Rebuild
|
|
210
|
+
// paths against the current user's DEST so any user on any host gets
|
|
211
|
+
// correct units without us having to re-ship plists per install target.
|
|
212
|
+
const scriptBase = path.basename(scriptPath);
|
|
213
|
+
const stdoutMatch = (xml.match(/<key>StandardOutPath<\/key>\s*<string>([^<]+)<\/string>/) || [])[1];
|
|
214
|
+
const stderrMatch = (xml.match(/<key>StandardErrorPath<\/key>\s*<string>([^<]+)<\/string>/) || [])[1];
|
|
215
|
+
const shortLabel = label.replace(/^com\.m13v\.social-/, '');
|
|
216
|
+
const stdout = `${DEST}/skill/logs/${stdoutMatch ? path.basename(stdoutMatch) : `launchd-${shortLabel}-stdout.log`}`;
|
|
217
|
+
const stderr = `${DEST}/skill/logs/${stderrMatch ? path.basename(stderrMatch) : `launchd-${shortLabel}-stderr.log`}`;
|
|
218
|
+
const runAtLoad = /<key>RunAtLoad<\/key>\s*<true\s*\/>/.test(xml);
|
|
219
|
+
jobs.push({
|
|
220
|
+
file: f,
|
|
221
|
+
label,
|
|
222
|
+
script: `${DEST}/skill/${scriptBase}`,
|
|
223
|
+
interval: sched.intervalSecs,
|
|
224
|
+
runAtLoad,
|
|
225
|
+
stdoutLog: stdout,
|
|
226
|
+
stderrLog: stderr,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
systemdDriver.generate({ jobs, outDir, env });
|
|
230
|
+
console.log(` translated ${jobs.length} launchd plists -> systemd units (skipped ${skipped})`);
|
|
231
|
+
return jobs.length;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Link every DEST/systemd/*.{service,timer} into ~/.config/systemd/user/ and
|
|
235
|
+
// reload the user daemon. Caller is expected to `systemctl --user enable --now
|
|
236
|
+
// <timer>` for each timer they actually want running; this mirrors how macOS
|
|
237
|
+
// setup leaves loading to the user via the SKILL.md wizard.
|
|
238
|
+
function installSystemdUnits() {
|
|
239
|
+
const driver = scheduler.driverFor('systemd');
|
|
240
|
+
const unitDir = path.join(DEST, 'systemd');
|
|
241
|
+
const agentsDir = platform.agentsDir();
|
|
242
|
+
if (!fs.existsSync(unitDir)) return;
|
|
243
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
244
|
+
const services = fs.readdirSync(unitDir).filter(f => f.endsWith('.service'));
|
|
245
|
+
let linked = 0;
|
|
246
|
+
for (const f of services) {
|
|
247
|
+
if (driver.install(path.join(unitDir, f), agentsDir)) linked++;
|
|
248
|
+
}
|
|
249
|
+
const r = spawnSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf8' });
|
|
250
|
+
if (r.status === 0) {
|
|
251
|
+
console.log(` linked ${linked} unit pair(s) into ${agentsDir}; systemctl --user daemon-reload OK`);
|
|
252
|
+
} else {
|
|
253
|
+
console.warn(` linked ${linked} unit pair(s); daemon-reload failed: ${(r.stderr || '').trim()}`);
|
|
254
|
+
}
|
|
255
|
+
const linger = spawnSync('loginctl', ['show-user', os.userInfo().username, '--property=Linger'], { encoding: 'utf8' });
|
|
256
|
+
if (!/Linger=yes/.test(linger.stdout || '')) {
|
|
257
|
+
console.log(' note: run `sudo loginctl enable-linger $USER` so timers fire when nobody is logged in');
|
|
153
258
|
}
|
|
154
|
-
console.log('
|
|
259
|
+
console.log(' next: systemctl --user enable --now <timer> for each job you want scheduled');
|
|
155
260
|
}
|
|
156
261
|
|
|
157
262
|
function init() {
|
|
@@ -175,8 +280,17 @@ function init() {
|
|
|
175
280
|
// Generate launchd plists with user's actual HOME
|
|
176
281
|
generatePlists();
|
|
177
282
|
|
|
283
|
+
// On Linux, derive systemd units from every plist and link them into
|
|
284
|
+
// ~/.config/systemd/user/. macOS install is unchanged.
|
|
285
|
+
if (platform.scheduler() === 'systemd') {
|
|
286
|
+
generateSystemdFromPlists();
|
|
287
|
+
installSystemdUnits();
|
|
288
|
+
}
|
|
289
|
+
|
|
178
290
|
// Install browser agent MCP configs + profile dirs (skips existing files)
|
|
179
291
|
installBrowserAgentConfigs();
|
|
292
|
+
// Register those MCP servers with Claude so they show up in `claude mcp list`.
|
|
293
|
+
registerBrowserAgentMcpServers();
|
|
180
294
|
|
|
181
295
|
// config.json — only if it doesn't exist
|
|
182
296
|
const configDest = path.join(DEST, 'config.json');
|
|
@@ -187,29 +301,18 @@ function init() {
|
|
|
187
301
|
console.log(' config.json exists — skipping');
|
|
188
302
|
}
|
|
189
303
|
|
|
190
|
-
// .env — only if it doesn't exist
|
|
304
|
+
// .env — only if it doesn't exist. Written from an in-package template so
|
|
305
|
+
// the NPM tarball no longer ships a credential-bearing .env.example file.
|
|
191
306
|
const envDest = path.join(DEST, '.env');
|
|
192
307
|
if (!fs.existsSync(envDest)) {
|
|
193
|
-
fs.
|
|
194
|
-
console.log(' created .env from template');
|
|
308
|
+
fs.writeFileSync(envDest, ENV_TEMPLATE);
|
|
309
|
+
console.log(' created .env from template (fill in DATABASE_URL and MOLTBOOK_API_KEY)');
|
|
195
310
|
} else {
|
|
196
311
|
console.log(' .env exists — skipping');
|
|
197
312
|
}
|
|
198
313
|
|
|
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
|
-
}
|
|
314
|
+
installPythonDeps();
|
|
315
|
+
installEngagementStylesSidecar();
|
|
213
316
|
|
|
214
317
|
// Remove stale skill/SKILL.md if it exists (SKILL.md lives at repo root only)
|
|
215
318
|
const skillMd = path.join(DEST, 'skill', 'SKILL.md');
|
|
@@ -259,8 +362,21 @@ function update() {
|
|
|
259
362
|
// Regenerate launchd plists with correct paths
|
|
260
363
|
generatePlists();
|
|
261
364
|
|
|
365
|
+
// Refresh systemd units on Linux so plist changes propagate.
|
|
366
|
+
if (platform.scheduler() === 'systemd') {
|
|
367
|
+
generateSystemdFromPlists();
|
|
368
|
+
installSystemdUnits();
|
|
369
|
+
}
|
|
370
|
+
|
|
262
371
|
// Top up browser agent configs (won't overwrite user customizations)
|
|
263
372
|
installBrowserAgentConfigs();
|
|
373
|
+
// Register any newly added MCP servers with Claude (idempotent).
|
|
374
|
+
registerBrowserAgentMcpServers();
|
|
375
|
+
|
|
376
|
+
// Refresh Python deps every update so version-bumps land on existing installs
|
|
377
|
+
// and the candidate-style sidecar gets merged (preserves VM-side candidates).
|
|
378
|
+
installPythonDeps();
|
|
379
|
+
installEngagementStylesSidecar();
|
|
264
380
|
|
|
265
381
|
// Remove stale skill/SKILL.md if it exists (SKILL.md lives at repo root only)
|
|
266
382
|
const skillMd = path.join(DEST, 'skill', 'SKILL.md');
|
|
@@ -281,18 +397,85 @@ function update() {
|
|
|
281
397
|
console.log('Update complete. config.json was preserved.');
|
|
282
398
|
}
|
|
283
399
|
|
|
400
|
+
// Install Python deps from requirements.txt (preferred) or fall back to the
|
|
401
|
+
// hardcoded list. Idempotent — pip3 install is a no-op when the package is
|
|
402
|
+
// already at the requested version. Playwright also needs the Chromium
|
|
403
|
+
// browser binary; we run `playwright install chromium` after the pip install.
|
|
404
|
+
function installPythonDeps() {
|
|
405
|
+
const reqPath = path.join(PKG_ROOT, 'requirements.txt');
|
|
406
|
+
const args = fs.existsSync(reqPath)
|
|
407
|
+
? ['install', '-r', reqPath, '-q']
|
|
408
|
+
: ['install', '-q', 'psycopg2-binary', 'playwright'];
|
|
409
|
+
console.log(' installing Python deps (psycopg2-binary, playwright, ...)');
|
|
410
|
+
const r = spawnSync('pip3', args, { stdio: 'inherit' });
|
|
411
|
+
if (r.status !== 0) {
|
|
412
|
+
console.warn(' WARNING: pip3 install failed — run manually:');
|
|
413
|
+
console.warn(` pip3 ${args.join(' ')}`);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
// Playwright needs its browser binary downloaded separately. Chromium
|
|
417
|
+
// is the only engine the repo uses today; skip Firefox/WebKit.
|
|
418
|
+
console.log(' installing Playwright Chromium binary (one-time, ~150MB)...');
|
|
419
|
+
const pw = spawnSync('python3', ['-m', 'playwright', 'install', 'chromium'], { stdio: 'inherit' });
|
|
420
|
+
if (pw.status !== 0) {
|
|
421
|
+
console.warn(' WARNING: playwright install chromium failed — run manually:');
|
|
422
|
+
console.warn(' python3 -m playwright install chromium');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Copy the candidate-style sidecar JSON into ~/social-autoposter/scripts/
|
|
427
|
+
// if missing; merge if present so VM-side invented candidates survive
|
|
428
|
+
// across updates. Promoted (status=active) entries from the shipped baseline
|
|
429
|
+
// always win.
|
|
430
|
+
function installEngagementStylesSidecar() {
|
|
431
|
+
const src = path.join(PKG_ROOT, 'scripts', 'engagement_styles_extra.json');
|
|
432
|
+
const dest = path.join(DEST, 'scripts', 'engagement_styles_extra.json');
|
|
433
|
+
if (!fs.existsSync(src)) return;
|
|
434
|
+
|
|
435
|
+
if (!fs.existsSync(dest)) {
|
|
436
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
437
|
+
fs.copyFileSync(src, dest);
|
|
438
|
+
console.log(' installed scripts/engagement_styles_extra.json');
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let shipped = {};
|
|
443
|
+
let local = {};
|
|
444
|
+
try { shipped = JSON.parse(fs.readFileSync(src, 'utf8')) || {}; } catch {}
|
|
445
|
+
try { local = JSON.parse(fs.readFileSync(dest, 'utf8')) || {}; } catch {}
|
|
446
|
+
|
|
447
|
+
// Start from local (preserves VM-only candidates), overlay shipped active
|
|
448
|
+
// entries so newly promoted styles always land. Shipped wins on conflict.
|
|
449
|
+
const merged = { ...local };
|
|
450
|
+
for (const [name, entry] of Object.entries(shipped)) {
|
|
451
|
+
merged[name] = entry;
|
|
452
|
+
}
|
|
453
|
+
fs.writeFileSync(dest, JSON.stringify(merged, null, 2) + '\n');
|
|
454
|
+
console.log(' merged scripts/engagement_styles_extra.json (shipped wins on conflict)');
|
|
455
|
+
}
|
|
456
|
+
|
|
284
457
|
const cmd = process.argv[2];
|
|
285
458
|
if (cmd === 'init') {
|
|
286
459
|
init();
|
|
287
460
|
} else if (cmd === 'update') {
|
|
288
461
|
update();
|
|
462
|
+
} else if (cmd === 'export-cookies') {
|
|
463
|
+
// Forward to cookie-helper with 'export' + remaining args
|
|
464
|
+
process.argv = [process.argv[0], process.argv[1], 'export', ...process.argv.slice(3)];
|
|
465
|
+
require('./cookie-helper.js');
|
|
466
|
+
} else if (cmd === 'import-cookies') {
|
|
467
|
+
// Forward to cookie-helper with 'import' + remaining args
|
|
468
|
+
process.argv = [process.argv[0], process.argv[1], 'import', ...process.argv.slice(3)];
|
|
469
|
+
require('./cookie-helper.js');
|
|
289
470
|
} else if (!cmd) {
|
|
290
471
|
require('./server.js');
|
|
291
472
|
} else {
|
|
292
473
|
console.log('social-autoposter — automated social posting for Claude agents');
|
|
293
474
|
console.log('');
|
|
294
475
|
console.log('Usage:');
|
|
295
|
-
console.log(' npx social-autoposter
|
|
296
|
-
console.log(' npx social-autoposter init
|
|
297
|
-
console.log(' npx social-autoposter update
|
|
476
|
+
console.log(' npx social-autoposter open the dashboard');
|
|
477
|
+
console.log(' npx social-autoposter init first-time setup');
|
|
478
|
+
console.log(' npx social-autoposter update update scripts, preserve config');
|
|
479
|
+
console.log(' npx social-autoposter export-cookies [dir] export browser cookies');
|
|
480
|
+
console.log(' npx social-autoposter import-cookies [dir] import browser cookies');
|
|
298
481
|
}
|