@m13v/s4l 1.6.197-rc.7
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 +143 -0
- package/SKILL.md +342 -0
- package/bin/cli.js +980 -0
- package/bin/cookie-helper.js +315 -0
- package/bin/platform.js +59 -0
- package/bin/scheduler/index.js +12 -0
- package/bin/scheduler/launchd.js +518 -0
- package/browser-agent-configs/all-agents-mcp.json +68 -0
- package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
- package/browser-agent-configs/linkedin-agent.json +17 -0
- package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
- package/browser-agent-configs/reddit-agent-mcp.json +16 -0
- package/browser-agent-configs/reddit-agent.json +17 -0
- package/browser-agent-configs/twitter-harness-mcp.json +18 -0
- package/config.example.json +45 -0
- package/mcp/dist/index.js +4212 -0
- package/mcp/dist/onboarding.js +200 -0
- package/mcp/dist/panel.html +176 -0
- package/mcp/dist/product-link.html +102 -0
- package/mcp/dist/repo.js +222 -0
- package/mcp/dist/runtime.js +1079 -0
- package/mcp/dist/screencast.js +323 -0
- package/mcp/dist/setup.js +545 -0
- package/mcp/dist/telemetry.js +306 -0
- package/mcp/dist/twitterAuth.js +138 -0
- package/mcp/dist/version.js +271 -0
- package/mcp/dist/version.json +4 -0
- package/mcp/install-runtime.mjs +70 -0
- package/mcp/install.mjs +169 -0
- package/mcp/manifest.json +80 -0
- package/mcp/menubar/dashboard_server.py +213 -0
- package/mcp/menubar/s4l_card.py +1314 -0
- package/mcp/menubar/s4l_log_relay.py +179 -0
- package/mcp/menubar/s4l_menubar.py +2439 -0
- package/mcp/menubar/s4l_state.py +891 -0
- package/mcp/package.json +34 -0
- package/mcp/shared/doctor.cjs +437 -0
- package/mcp/shared/onboarding-ledger.cjs +324 -0
- package/mcp-servers/browser-harness/server.py +968 -0
- package/package.json +160 -0
- package/requirements.txt +20 -0
- package/scripts/_compute_allowlist.py +58 -0
- package/scripts/_db_update.py +20 -0
- package/scripts/_filt.py +9 -0
- package/scripts/_li_notif_match.py +76 -0
- package/scripts/_li_notif_orchestrate.py +126 -0
- package/scripts/_lock_preempt_test.py +60 -0
- package/scripts/_run_icp_precheck.py +57 -0
- package/scripts/a16z_pearx_calendar_reminders.py +99 -0
- package/scripts/account_resolver.py +141 -0
- package/scripts/active_campaigns.py +114 -0
- package/scripts/active_users.py +190 -0
- package/scripts/amplitude_24h_signups.py +468 -0
- package/scripts/amplitude_signups.py +177 -0
- package/scripts/apply_onboarding_selections.py +131 -0
- package/scripts/audience_pages.py +243 -0
- package/scripts/audit_helper.py +120 -0
- package/scripts/author_history_block.py +353 -0
- package/scripts/autopilot_stall_watch.py +284 -0
- package/scripts/backfill_twitter_attempts_topic.py +81 -0
- package/scripts/backfill_twitter_log_post_no_id.py +322 -0
- package/scripts/bench_dashboard.sh +138 -0
- package/scripts/bh_send.py +39 -0
- package/scripts/build_persona.py +409 -0
- package/scripts/bulk_icp.py +18 -0
- package/scripts/campaign_bump.py +51 -0
- package/scripts/capture_thread_media.py +288 -0
- package/scripts/check_browser_lock_health.sh +81 -0
- package/scripts/check_external_pool_depth.py +253 -0
- package/scripts/check_unread_web_chats.py +28 -0
- package/scripts/claim_web_chat.py +47 -0
- package/scripts/classify_run_error.py +158 -0
- package/scripts/claude_job.py +988 -0
- package/scripts/clean_stale_singleton.sh +56 -0
- package/scripts/cleanup_harness_tabs.py +68 -0
- package/scripts/copy_browser_cookies.py +454 -0
- package/scripts/counterparty_history.py +350 -0
- package/scripts/db.py +57 -0
- package/scripts/discover_claude_profiles.py +120 -0
- package/scripts/discover_linkedin_candidates.py +984 -0
- package/scripts/dm_conversation.py +682 -0
- package/scripts/dm_db_update.py +69 -0
- package/scripts/dm_engage_helper.py +161 -0
- package/scripts/dm_outreach_helper.py +147 -0
- package/scripts/dm_outreach_twitter_helper.py +129 -0
- package/scripts/dm_send_log.py +106 -0
- package/scripts/dm_short_links.py +1084 -0
- package/scripts/dump_web_chat_history.py +47 -0
- package/scripts/engage_github.py +640 -0
- package/scripts/engage_reddit.py +1235 -0
- package/scripts/engage_twitter_helper.py +301 -0
- package/scripts/engagement_styles.py +1787 -0
- package/scripts/enrich_twitter_candidates.py +82 -0
- package/scripts/feedback_digest.py +448 -0
- package/scripts/fetch_prospect_profile.py +312 -0
- package/scripts/fetch_twitter_t1.py +134 -0
- package/scripts/find_threads.py +530 -0
- package/scripts/follow_gate_log.py +59 -0
- package/scripts/funnel_per_day.py +194 -0
- package/scripts/generate_daily_human_style.py +494 -0
- package/scripts/generation_trace.py +173 -0
- package/scripts/get_run_cost.py +107 -0
- package/scripts/github_engage_helper.py +93 -0
- package/scripts/github_tools.py +509 -0
- package/scripts/harness_overlay.py +556 -0
- package/scripts/harvest_twitter_following.py +243 -0
- package/scripts/heartbeat.sh +70 -0
- package/scripts/history_context.py +284 -0
- package/scripts/http_api.py +206 -0
- package/scripts/human_dm_replies_helper.py +169 -0
- package/scripts/identity.py +302 -0
- package/scripts/ig_batch_creator.sh +93 -0
- package/scripts/ig_post_type_picker.py +243 -0
- package/scripts/ig_scrape_transcribe.sh +91 -0
- package/scripts/ingest_human_dm_replies.py +271 -0
- package/scripts/ingest_web_chat_replies.py +229 -0
- package/scripts/install_fleet.py +187 -0
- package/scripts/invent_mcp_server.py +350 -0
- package/scripts/invent_topics.py +1462 -0
- package/scripts/learned_preferences.py +263 -0
- package/scripts/li_discovery.py +161 -0
- package/scripts/link_edit_helper.py +142 -0
- package/scripts/link_tail.py +592 -0
- package/scripts/linkedin_api.py +561 -0
- package/scripts/linkedin_browser.py +730 -0
- package/scripts/linkedin_cooldown.py +128 -0
- package/scripts/linkedin_exclusions.py +234 -0
- package/scripts/linkedin_killswitch.py +1333 -0
- package/scripts/linkedin_search_topic_schema.py +49 -0
- package/scripts/linkedin_unipile.py +658 -0
- package/scripts/linkedin_url.py +228 -0
- package/scripts/log_claude_session.py +636 -0
- package/scripts/log_draft.py +143 -0
- package/scripts/log_linkedin_search_attempts.py +126 -0
- package/scripts/log_post.py +651 -0
- package/scripts/log_run.py +364 -0
- package/scripts/log_thread_media.py +108 -0
- package/scripts/log_twitter_search_attempts.py +150 -0
- package/scripts/log_twitter_skips.py +211 -0
- package/scripts/lookup_post.py +78 -0
- package/scripts/mark_web_chat_processed.py +32 -0
- package/scripts/mcp_lock_proxy.py +370 -0
- package/scripts/memory_snapshot.py +972 -0
- package/scripts/merge_review_queue.py +215 -0
- package/scripts/mint_external_pool.py +182 -0
- package/scripts/mint_kent_pool.py +249 -0
- package/scripts/moltbook_post.py +320 -0
- package/scripts/moltbook_tools.py +159 -0
- package/scripts/pending_threads.py +188 -0
- package/scripts/pick_ig_account.py +177 -0
- package/scripts/pick_project.py +208 -0
- package/scripts/pick_search_topic.py +771 -0
- package/scripts/pick_thread_target.py +279 -0
- package/scripts/pick_twitter_thread_target.py +202 -0
- package/scripts/podlog_fetch_batch.sh +32 -0
- package/scripts/post_github.py +1311 -0
- package/scripts/post_reddit.py +2668 -0
- package/scripts/precompute_dashboard_stats.py +204 -0
- package/scripts/preflight.sh +297 -0
- package/scripts/progress.py +88 -0
- package/scripts/project_excludes.py +353 -0
- package/scripts/project_slugs.py +91 -0
- package/scripts/project_stats.py +241 -0
- package/scripts/project_stats_json.py +1563 -0
- package/scripts/project_topics.py +192 -0
- package/scripts/qualified_query_bank.py +436 -0
- package/scripts/reap_stale_claude_sessions.py +867 -0
- package/scripts/reddit_browser.py +2549 -0
- package/scripts/reddit_browser_fetch.py +141 -0
- package/scripts/reddit_browser_lock.py +593 -0
- package/scripts/reddit_chat_sync.py +710 -0
- package/scripts/reddit_query_bank.py +200 -0
- package/scripts/reddit_threads_helper.py +151 -0
- package/scripts/reddit_tools.py +956 -0
- package/scripts/refresh_instagram_tokens.py +280 -0
- package/scripts/release-mcpb.sh +497 -0
- package/scripts/reply_db.py +334 -0
- package/scripts/reply_insert.py +98 -0
- package/scripts/reply_risk_digest.py +761 -0
- package/scripts/reset-test-machine.sh +602 -0
- package/scripts/restore_twitter_session.py +177 -0
- package/scripts/ripen_reddit_plan.py +478 -0
- package/scripts/run_claude.sh +433 -0
- package/scripts/run_moltbook_cycle.py +555 -0
- package/scripts/s4l_box_update.sh +226 -0
- package/scripts/s4l_channel.py +103 -0
- package/scripts/s4l_ctl.sh +75 -0
- package/scripts/s4l_env.py +47 -0
- package/scripts/saps_activity.py +126 -0
- package/scripts/saps_mode.py +328 -0
- package/scripts/scan_dm_candidates.py +580 -0
- package/scripts/scan_github_replies.py +168 -0
- package/scripts/scan_instagram_comments.py +481 -0
- package/scripts/scan_moltbook_replies.py +252 -0
- package/scripts/scan_pii.py +190 -0
- package/scripts/scan_reddit_replies.py +377 -0
- package/scripts/scan_twitter_mentions_browser.py +327 -0
- package/scripts/scan_twitter_thread_followups.py +299 -0
- package/scripts/scan_x_profile.py +384 -0
- package/scripts/schedule_state.py +202 -0
- package/scripts/scheduled_tasks_snapshot.py +123 -0
- package/scripts/score_linkedin_candidates.py +419 -0
- package/scripts/score_twitter_candidates.py +718 -0
- package/scripts/scrape_linkedin_comment_stats.py +1755 -0
- package/scripts/scrape_linkedin_stats_browser.py +52 -0
- package/scripts/scrape_reddit_views.py +365 -0
- package/scripts/seed_search_queries.py +453 -0
- package/scripts/seed_search_topics.py +127 -0
- package/scripts/send_web_chat_reply.py +130 -0
- package/scripts/sentry_init.py +128 -0
- package/scripts/setup_twitter_auth.py +1320 -0
- package/scripts/snapshot.py +583 -0
- package/scripts/stats.py +2702 -0
- package/scripts/stats_helper.py +52 -0
- package/scripts/strike_alert.py +783 -0
- package/scripts/sweep_post_link_clicks.py +107 -0
- package/scripts/sync_ig_to_posts.py +147 -0
- package/scripts/test_browser_lock.py +189 -0
- package/scripts/test_installation_api.sh +52 -0
- package/scripts/test_percard_posting.py +142 -0
- package/scripts/top_dud_linkedin_queries.py +71 -0
- package/scripts/top_dud_reddit_queries.py +67 -0
- package/scripts/top_dud_twitter_queries.py +71 -0
- package/scripts/top_dud_twitter_topics.py +102 -0
- package/scripts/top_linkedin_queries.py +55 -0
- package/scripts/top_omitted_reddit_topics.py +91 -0
- package/scripts/top_performers.py +588 -0
- package/scripts/top_search_topics.py +180 -0
- package/scripts/top_twitter_queries.py +190 -0
- package/scripts/twitter_access_check.py +382 -0
- package/scripts/twitter_account.py +41 -0
- package/scripts/twitter_batch_phase.py +126 -0
- package/scripts/twitter_browser.py +2804 -0
- package/scripts/twitter_cookie_mirror.py +130 -0
- package/scripts/twitter_cycle_helper.py +310 -0
- package/scripts/twitter_gen_links.py +287 -0
- package/scripts/twitter_post_plan.py +1188 -0
- package/scripts/twitter_scan.py +324 -0
- package/scripts/twitter_supply_signal.py +57 -0
- package/scripts/twitter_threads_helper.py +152 -0
- package/scripts/unclaim_web_chat.py +29 -0
- package/scripts/update_instagram_stats.py +261 -0
- package/scripts/update_linkedin_stats_from_feed.py +328 -0
- package/scripts/version.py +72 -0
- package/scripts/watchdog_hung_runs.py +343 -0
- package/scripts/write_generation_trace.py +73 -0
- package/setup/SKILL.md +277 -0
- package/skill/amplitude-24h-signups.sh +38 -0
- package/skill/archive-old-logs.sh +40 -0
- package/skill/audit-dm-staleness.sh +42 -0
- package/skill/audit-linkedin.sh +14 -0
- package/skill/audit-moltbook.sh +4 -0
- package/skill/audit-reddit-resurrect.sh +67 -0
- package/skill/audit-reddit.sh +4 -0
- package/skill/audit-twitter.sh +4 -0
- package/skill/audit.sh +287 -0
- package/skill/backfill-twitter-attempts-topic.sh +19 -0
- package/skill/backfill-twitter-ghost-posts.sh +24 -0
- package/skill/check-external-pool-depth.sh +7 -0
- package/skill/check-web-chats.sh +203 -0
- package/skill/dm-outreach-linkedin.sh +250 -0
- package/skill/dm-outreach-reddit.sh +274 -0
- package/skill/dm-outreach-twitter.sh +265 -0
- package/skill/engage-dm-replies-linkedin.sh +4 -0
- package/skill/engage-dm-replies-reddit.sh +4 -0
- package/skill/engage-dm-replies-twitter.sh +4 -0
- package/skill/engage-dm-replies.sh +1597 -0
- package/skill/engage-linkedin.sh +581 -0
- package/skill/engage-moltbook.sh +36 -0
- package/skill/engage-reddit.sh +146 -0
- package/skill/engage-twitter.sh +467 -0
- package/skill/github-engage.sh +176 -0
- package/skill/ingest-web-chat-replies.sh +38 -0
- package/skill/invent-supply-test.sh +100 -0
- package/skill/invent-topics.sh +50 -0
- package/skill/lib/linkedin-backend.sh +364 -0
- package/skill/lib/platform.sh +48 -0
- package/skill/lib/reddit-backend.sh +234 -0
- package/skill/lib/twitter-backend.sh +314 -0
- package/skill/link-edit-github.sh +136 -0
- package/skill/link-edit-moltbook.sh +117 -0
- package/skill/link-edit-reddit.sh +201 -0
- package/skill/linkedin-presence.sh +182 -0
- package/skill/linkedin-recovery.sh +282 -0
- package/skill/lock.sh +647 -0
- package/skill/memory-snapshot.sh +39 -0
- package/skill/precompute-stats.sh +35 -0
- package/skill/prewarm-funnel.sh +104 -0
- package/skill/refresh-instagram-tokens.sh +57 -0
- package/skill/refresh-twitter-following.sh +52 -0
- package/skill/reply-risk-digest.sh +31 -0
- package/skill/run-cycle-update-guard.sh +44 -0
- package/skill/run-draft-and-publish.sh +123 -0
- package/skill/run-generate-daily-style.sh +50 -0
- package/skill/run-github-launchd.sh +62 -0
- package/skill/run-github.sh +102 -0
- package/skill/run-instagram-daily.sh +149 -0
- package/skill/run-instagram-render.sh +875 -0
- package/skill/run-linkedin-launchd.sh +81 -0
- package/skill/run-linkedin-unipile.sh +130 -0
- package/skill/run-linkedin.sh +1593 -0
- package/skill/run-moltbook-launchd.sh +61 -0
- package/skill/run-moltbook.sh +38 -0
- package/skill/run-overlay-watch.sh +100 -0
- package/skill/run-reddit-search-launchd.sh +64 -0
- package/skill/run-reddit-search.sh +505 -0
- package/skill/run-reddit-threads-double.sh +32 -0
- package/skill/run-reddit-threads.sh +847 -0
- package/skill/run-scan-moltbook-replies.sh +57 -0
- package/skill/run-twitter-cycle-launchd.sh +63 -0
- package/skill/run-twitter-cycle-singleton.sh +62 -0
- package/skill/run-twitter-cycle.sh +2408 -0
- package/skill/run-twitter-threads.sh +592 -0
- package/skill/scan-instagram-replies.sh +61 -0
- package/skill/scan-twitter-followups.sh +57 -0
- package/skill/social-autoposter-update.sh +66 -0
- package/skill/stats-instagram.sh +72 -0
- package/skill/stats-linkedin.sh +271 -0
- package/skill/stats-moltbook.sh +4 -0
- package/skill/stats-reddit.sh +4 -0
- package/skill/stats-twitter.sh +4 -0
- package/skill/stats.sh +521 -0
- package/skill/strike-alert.sh +18 -0
- package/skill/styles.sh +87 -0
- package/skill/sweep-link-clicks.sh +40 -0
- package/skill/topics.sh +51 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const scheduler = require('./scheduler');
|
|
10
|
+
const { formatDoctorReport, runDoctorSync } = require('../mcp/shared/doctor.cjs');
|
|
11
|
+
const { recordDoctorReport } = require('../mcp/shared/onboarding-ledger.cjs');
|
|
12
|
+
|
|
13
|
+
const DEST = path.join(os.homedir(), 'social-autoposter');
|
|
14
|
+
const PKG_ROOT = path.join(__dirname, '..');
|
|
15
|
+
const HOME = os.homedir();
|
|
16
|
+
|
|
17
|
+
// Files/dirs to copy from npm package to ~/social-autoposter
|
|
18
|
+
const COPY_TARGETS = [
|
|
19
|
+
'scripts',
|
|
20
|
+
'config.example.json',
|
|
21
|
+
'requirements.txt',
|
|
22
|
+
'SKILL.md',
|
|
23
|
+
'skill',
|
|
24
|
+
'setup',
|
|
25
|
+
'browser-agent-configs',
|
|
26
|
+
'mcp-servers',
|
|
27
|
+
'mcp',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Never overwrite these user files during update
|
|
31
|
+
const USER_FILES = new Set(['config.json', '.env', 'SKILL.md']);
|
|
32
|
+
|
|
33
|
+
// Browser agent config templates -> install path under ~/.claude/browser-agent-configs/
|
|
34
|
+
// twitter-harness replaces the retired twitter-agent (2026-05-19). The harness
|
|
35
|
+
// runs a CDP-driven real Chrome on port 9555 backed by an MCP stdio server at
|
|
36
|
+
// ~/.claude/mcp-servers/browser-harness/server.py. installBrowserHarness()
|
|
37
|
+
// below provisions the supporting bits (uv, browser-harness CLI, mcp pkg).
|
|
38
|
+
const BROWSER_AGENT_CONFIGS = [
|
|
39
|
+
'reddit-agent-mcp.json',
|
|
40
|
+
'reddit-agent.json',
|
|
41
|
+
'linkedin-agent-mcp.json',
|
|
42
|
+
'linkedin-agent.json',
|
|
43
|
+
'twitter-harness-mcp.json',
|
|
44
|
+
'linkedin-harness-mcp.json',
|
|
45
|
+
'all-agents-mcp.json',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const BROWSER_PROFILES = ['reddit', 'linkedin', 'browser-harness'];
|
|
49
|
+
|
|
50
|
+
function copyDir(src, dest) {
|
|
51
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
52
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
53
|
+
const srcPath = path.join(src, entry.name);
|
|
54
|
+
const destPath = path.join(dest, entry.name);
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
copyDir(srcPath, destPath);
|
|
57
|
+
} else {
|
|
58
|
+
fs.copyFileSync(srcPath, destPath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function linkOrRelink(target, linkPath) {
|
|
64
|
+
try { fs.rmSync(linkPath, { recursive: true, force: true }); } catch {}
|
|
65
|
+
fs.symlinkSync(target, linkPath);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Locate uv (Astral's Python launcher). The browser-harness MCP server is
|
|
69
|
+
// shebanged through uv so it can pull `mcp` on first run without polluting
|
|
70
|
+
// the system Python. Returns the absolute path if found, or empty string.
|
|
71
|
+
function findUvBin() {
|
|
72
|
+
const candidates = [
|
|
73
|
+
path.join(HOME, '.local', 'bin', 'uv'),
|
|
74
|
+
'/opt/homebrew/bin/uv',
|
|
75
|
+
'/usr/local/bin/uv',
|
|
76
|
+
'/usr/bin/uv',
|
|
77
|
+
];
|
|
78
|
+
for (const c of candidates) {
|
|
79
|
+
if (fs.existsSync(c)) return c;
|
|
80
|
+
}
|
|
81
|
+
const which = spawnSync('command', ['-v', 'uv'], { shell: true, encoding: 'utf8' });
|
|
82
|
+
const found = (which.stdout || '').trim().split('\n')[0];
|
|
83
|
+
return found && fs.existsSync(found) ? found : '';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Locate the Python the MCP server is actually configured to run (S4L_PYTHON).
|
|
87
|
+
// mcp/install.mjs picks /opt/homebrew/bin/python3 (or /usr/local/bin/python3)
|
|
88
|
+
// and stamps it into the MCP config, so Python deps MUST be installed into that
|
|
89
|
+
// SAME interpreter. Bare `pip3`/`python3` on macOS usually resolves to the
|
|
90
|
+
// Xcode CLT system python (3.9.x with pip 21.x), which is both the wrong target
|
|
91
|
+
// and too old to understand --break-system-packages. Falls back to `python3`.
|
|
92
|
+
function findPythonBin() {
|
|
93
|
+
const candidates = ['/opt/homebrew/bin/python3', '/usr/local/bin/python3'];
|
|
94
|
+
for (const c of candidates) {
|
|
95
|
+
if (fs.existsSync(c)) return c;
|
|
96
|
+
}
|
|
97
|
+
const which = spawnSync('command', ['-v', 'python3'], { shell: true, encoding: 'utf8' });
|
|
98
|
+
const found = (which.stdout || '').trim().split('\n')[0];
|
|
99
|
+
return found && fs.existsSync(found) ? found : 'python3';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// True if `<pythonBin> -m pip` is new enough (pip >= 23.0) to accept
|
|
103
|
+
// --break-system-packages. Older pips treat the flag as an unknown option and
|
|
104
|
+
// hard-fail, so we must not pass it blindly on the retry.
|
|
105
|
+
function pipSupportsBreakSystemPackages(pythonBin) {
|
|
106
|
+
const v = spawnSync(pythonBin, ['-m', 'pip', '--version'], { encoding: 'utf8' });
|
|
107
|
+
const m = (v.stdout || '').match(/pip\s+(\d+)\.(\d+)/);
|
|
108
|
+
if (!m) return false;
|
|
109
|
+
return parseInt(m[1], 10) >= 23;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// True if the interpreter carries a PEP 668 EXTERNALLY-MANAGED marker in its
|
|
113
|
+
// stdlib dir (Homebrew python, Debian/Ubuntu 23+). On these, a bare
|
|
114
|
+
// `pip install` is GUARANTEED to fail with a loud "externally-managed-environment"
|
|
115
|
+
// wall of text. Detecting it up front lets pipInstall skip that doomed first
|
|
116
|
+
// attempt and go straight to --break-system-packages, so init output stays clean
|
|
117
|
+
// and doesn't falsely look like a failed dependency install when it recovers.
|
|
118
|
+
function pipIsExternallyManaged(pythonBin) {
|
|
119
|
+
const r = spawnSync(pythonBin, ['-c',
|
|
120
|
+
"import os,sys,sysconfig\n" +
|
|
121
|
+
"p=os.path.join(sysconfig.get_path('stdlib'),'EXTERNALLY-MANAGED')\n" +
|
|
122
|
+
"sys.exit(0 if os.path.exists(p) else 1)",
|
|
123
|
+
]);
|
|
124
|
+
return r.status === 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Install Python packages into a specific interpreter via `<py> -m pip install`.
|
|
128
|
+
// Behaviour by environment:
|
|
129
|
+
// - PEP 668 externally-managed interpreter (Homebrew python, Debian/Ubuntu 23+)
|
|
130
|
+
// with pip>=23: go STRAIGHT to --break-system-packages. The bare attempt
|
|
131
|
+
// would always fail loudly with externally-managed-environment, which made
|
|
132
|
+
// init look like "Python deps failed" even though the (silent) retry actually
|
|
133
|
+
// installed everything. No doomed first attempt, no false-alarm output.
|
|
134
|
+
// - Everything else: bare attempt, then retry with --break-system-packages only
|
|
135
|
+
// if it failed and pip supports the flag.
|
|
136
|
+
// Returns the spawnSync result of the last attempt.
|
|
137
|
+
function pipInstall(pythonBin, args) {
|
|
138
|
+
const base = ['-m', 'pip', 'install', ...args];
|
|
139
|
+
if (pipIsExternallyManaged(pythonBin) && pipSupportsBreakSystemPackages(pythonBin)) {
|
|
140
|
+
return spawnSync(pythonBin, [...base, '--break-system-packages'], { stdio: 'inherit' });
|
|
141
|
+
}
|
|
142
|
+
let r = spawnSync(pythonBin, base, { stdio: 'inherit' });
|
|
143
|
+
if (r.status !== 0 && pipSupportsBreakSystemPackages(pythonBin)) {
|
|
144
|
+
r = spawnSync(pythonBin, [...base, '--break-system-packages'], { stdio: 'inherit' });
|
|
145
|
+
}
|
|
146
|
+
return r;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function installBrowserAgentConfigs() {
|
|
150
|
+
const nodeBin = path.dirname(process.execPath);
|
|
151
|
+
const uvBin = findUvBin() || path.join(HOME, '.local', 'bin', 'uv');
|
|
152
|
+
const srcDir = path.join(PKG_ROOT, 'browser-agent-configs');
|
|
153
|
+
const destDir = path.join(HOME, '.claude', 'browser-agent-configs');
|
|
154
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
155
|
+
|
|
156
|
+
let installed = 0;
|
|
157
|
+
let skipped = 0;
|
|
158
|
+
for (const name of BROWSER_AGENT_CONFIGS) {
|
|
159
|
+
const src = path.join(srcDir, name);
|
|
160
|
+
const dest = path.join(destDir, name);
|
|
161
|
+
if (!fs.existsSync(src)) continue;
|
|
162
|
+
if (fs.existsSync(dest)) {
|
|
163
|
+
skipped++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const tpl = fs.readFileSync(src, 'utf8');
|
|
167
|
+
const out = tpl
|
|
168
|
+
.replace(/__HOME__/g, HOME)
|
|
169
|
+
.replace(/__NODE_BIN__/g, nodeBin)
|
|
170
|
+
.replace(/__UV_BIN__/g, uvBin);
|
|
171
|
+
fs.writeFileSync(dest, out);
|
|
172
|
+
installed++;
|
|
173
|
+
}
|
|
174
|
+
console.log(` browser agent configs -> ${destDir} (installed ${installed}, skipped ${skipped} existing)`);
|
|
175
|
+
|
|
176
|
+
// Create empty persistent profile dirs so Playwright has somewhere to land cookies
|
|
177
|
+
const profilesDir = path.join(HOME, '.claude', 'browser-profiles');
|
|
178
|
+
fs.mkdirSync(profilesDir, { recursive: true });
|
|
179
|
+
for (const p of BROWSER_PROFILES) {
|
|
180
|
+
fs.mkdirSync(path.join(profilesDir, p), { recursive: true });
|
|
181
|
+
}
|
|
182
|
+
console.log(` browser profile dirs ready -> ${profilesDir}/{${BROWSER_PROFILES.join(',')}}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Detect whether we are running inside an AppMaker E2B VM. AppMaker provisions
|
|
186
|
+
// a Chromium on port 9222 behind the SOAX residential proxy at 127.0.0.1:3003,
|
|
187
|
+
// and that Chromium is the one the user logs into via the AppMaker UI (profile
|
|
188
|
+
// /root/.chromium-profile). The browser-harness Chrome on port 9555 with its
|
|
189
|
+
// own (logged-out, un-proxied) profile is wrong for this host, so we:
|
|
190
|
+
// 1. skip installBrowserHarness() entirely (saves disk + avoids a second
|
|
191
|
+
// headless Chrome ever spawning).
|
|
192
|
+
// 2. write ~/.social-autoposter-env so skill/lib/twitter-backend.sh sources
|
|
193
|
+
// TWITTER_CDP_URL=http://127.0.0.1:9222 instead of the default 9555.
|
|
194
|
+
// Detection: presence of /opt/startup.sh (the AppMaker bootstrap script that
|
|
195
|
+
// only exists on these VMs) AND a live HTTP response on 127.0.0.1:9222.
|
|
196
|
+
function isAppMakerVm() {
|
|
197
|
+
if (process.platform !== 'linux') return false;
|
|
198
|
+
if (!fs.existsSync('/opt/startup.sh')) return false;
|
|
199
|
+
// Probe Chromium DevTools on 9222. 2s timeout; if it answers, we're on AppMaker.
|
|
200
|
+
const probe = spawnSync('curl', ['-sf', '--max-time', '2', '-o', '/dev/null', 'http://127.0.0.1:9222/json/version'], { stdio: 'ignore' });
|
|
201
|
+
return probe.status === 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// VM / AppMaker support is strictly opt-in. A normal `init`/`update` (the
|
|
205
|
+
// macOS user path) installs none of it — no apt-get, no :9222 CDP env file, no
|
|
206
|
+
// AppMaker MCP port overrides. It activates only when explicitly requested:
|
|
207
|
+
// - env SA_VM=1 (or SOCIAL_AUTOPOSTER_VM=1)
|
|
208
|
+
// - flag --vm on the command line
|
|
209
|
+
// - a persisted marker written by `bootstrap-vm` (so later `update`s on the
|
|
210
|
+
// same VM stay in VM mode without re-passing the flag)
|
|
211
|
+
// - a genuine AppMaker VM (linux + /opt/startup.sh + live :9222) — kept as a
|
|
212
|
+
// fallback so the existing mk0r bootstrap keeps working untouched. This can
|
|
213
|
+
// never be true on a user's Mac.
|
|
214
|
+
const VM_MARKER = path.join(HOME, '.social-autoposter', 'vm-mode');
|
|
215
|
+
function vmModeEnabled() {
|
|
216
|
+
if (process.env.SA_VM === '1' || process.env.SOCIAL_AUTOPOSTER_VM === '1') return true;
|
|
217
|
+
if (process.argv.includes('--vm')) return true;
|
|
218
|
+
try { if (fs.existsSync(VM_MARKER)) return true; } catch { /* ignore */ }
|
|
219
|
+
return isAppMakerVm();
|
|
220
|
+
}
|
|
221
|
+
function enableVmMode() {
|
|
222
|
+
try {
|
|
223
|
+
fs.mkdirSync(path.dirname(VM_MARKER), { recursive: true });
|
|
224
|
+
fs.writeFileSync(VM_MARKER, 'enabled\n');
|
|
225
|
+
} catch { /* best-effort; vmModeEnabled() still honors env/flag/probe */ }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Write ~/.social-autoposter-env so skill/lib/twitter-backend.sh picks up the
|
|
229
|
+
// AppMaker-specific TWITTER_CDP_URL before its `${VAR:-default}` fallback hits.
|
|
230
|
+
// Idempotent: rewrites the file every invocation so a config edit on the VM
|
|
231
|
+
// can't drift away from what cli.js intends.
|
|
232
|
+
function writeAppMakerEnvFile(handleFromDb) {
|
|
233
|
+
const envPath = path.join(HOME, '.social-autoposter-env');
|
|
234
|
+
// Source of truth for the handle is the DB (social_accounts.handle keyed by
|
|
235
|
+
// vm_session_key). bootstrap-vm passes it in. Fallback: preserve a previously
|
|
236
|
+
// set value across rewrites if no DB-sourced handle was provided (matters
|
|
237
|
+
// when this runs from `social-autoposter update` without a fresh DB fetch).
|
|
238
|
+
let preservedHandle = String(handleFromDb || '').trim().replace(/^@/, '');
|
|
239
|
+
if (!preservedHandle) {
|
|
240
|
+
try {
|
|
241
|
+
const prev = fs.readFileSync(envPath, 'utf8');
|
|
242
|
+
const m = prev.match(/^\s*export\s+AUTOPOSTER_TWITTER_HANDLE=(.+)\s*$/m);
|
|
243
|
+
if (m) preservedHandle = m[1].trim();
|
|
244
|
+
} catch { /* no prior file */ }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const lines = [
|
|
248
|
+
'# social-autoposter per-host env overrides',
|
|
249
|
+
'# Auto-generated by social-autoposter init/update on AppMaker E2B VMs.',
|
|
250
|
+
'# Edit by hand only if you know what you are doing; it gets rewritten on every update.',
|
|
251
|
+
'',
|
|
252
|
+
'# Point twitter pipeline at AppMaker\'s proxied Chromium (SOAX residential exit',
|
|
253
|
+
'# at 127.0.0.1:3003) instead of the harness Chrome on 9555. The Chromium on',
|
|
254
|
+
'# 9222 is the one the user logs into via the AppMaker UI.',
|
|
255
|
+
'export TWITTER_CDP_URL="http://127.0.0.1:9222"',
|
|
256
|
+
'',
|
|
257
|
+
'# AppMaker VMs run as root and the appmaker template sets Claude defaultMode',
|
|
258
|
+
'# to bypassPermissions. Claude CLI refuses bypassPermissions under root for',
|
|
259
|
+
'# security reasons UNLESS IS_SANDBOX=1 is set. Without this, every `claude -p`',
|
|
260
|
+
'# call in the pipeline exits immediately with no output (cost=$0.00, 16s) and',
|
|
261
|
+
'# Phase 1 reports envelope parse error / phase1_no_tweets.',
|
|
262
|
+
'export IS_SANDBOX=1',
|
|
263
|
+
'',
|
|
264
|
+
];
|
|
265
|
+
if (preservedHandle) {
|
|
266
|
+
lines.push(
|
|
267
|
+
'# Which Twitter handle this sandbox posts as. Durable home for the handle',
|
|
268
|
+
'# because config.json is reseeded on E2B sandbox substitution. Read by',
|
|
269
|
+
'# twitter_account.resolve_handle() (cycle scoping + session restore).',
|
|
270
|
+
`export AUTOPOSTER_TWITTER_HANDLE=${preservedHandle}`,
|
|
271
|
+
'',
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const body = lines.join('\n');
|
|
275
|
+
fs.writeFileSync(envPath, body);
|
|
276
|
+
console.log(` AppMaker VM detected -> wrote ${envPath} (TWITTER_CDP_URL=http://127.0.0.1:9222${preservedHandle ? `, AUTOPOSTER_TWITTER_HANDLE=${preservedHandle}` : ''})`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// AppMaker VMs: symlink /root/.chromium-profile → ~/.claude/browser-profiles/browser-harness
|
|
280
|
+
// so the appmaker-managed Chrome on port 9222 (launched by /opt/startup.sh with
|
|
281
|
+
// --user-data-dir=/root/.chromium-profile) actually opens the HARNESS profile,
|
|
282
|
+
// which is where our @<handle> Twitter login lives. Without this symlink, the
|
|
283
|
+
// appmaker Chrome opens a fresh empty profile and the pipeline talks to a
|
|
284
|
+
// logged-out browser. Combined with ENABLE_ROOT_VOLUME=1 on the Cloud Run
|
|
285
|
+
// host, the profile (and its cookies) now survives sandbox substitution.
|
|
286
|
+
// Idempotent: if already symlinked correctly, no-op. If a real directory
|
|
287
|
+
// exists, back it up (so any local-only browser cache isn't lost) and replace.
|
|
288
|
+
function linkAppMakerHarnessProfile() {
|
|
289
|
+
const harnessProfile = path.join(HOME, '.claude', 'browser-profiles', 'browser-harness');
|
|
290
|
+
const appmakerProfile = '/root/.chromium-profile';
|
|
291
|
+
try {
|
|
292
|
+
fs.mkdirSync(harnessProfile, { recursive: true });
|
|
293
|
+
let stat = null;
|
|
294
|
+
try { stat = fs.lstatSync(appmakerProfile); } catch { /* not present */ }
|
|
295
|
+
if (stat && stat.isSymbolicLink()) {
|
|
296
|
+
const target = fs.readlinkSync(appmakerProfile);
|
|
297
|
+
if (target === harnessProfile) {
|
|
298
|
+
console.log(` AppMaker profile already symlinked: ${appmakerProfile} -> ${harnessProfile}`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
fs.unlinkSync(appmakerProfile);
|
|
302
|
+
} else if (stat && stat.isDirectory()) {
|
|
303
|
+
const backup = `${appmakerProfile}.replaced-by-symlink-${Date.now()}`;
|
|
304
|
+
fs.renameSync(appmakerProfile, backup);
|
|
305
|
+
console.log(` backed up existing ${appmakerProfile} -> ${backup}`);
|
|
306
|
+
}
|
|
307
|
+
fs.symlinkSync(harnessProfile, appmakerProfile);
|
|
308
|
+
console.log(` symlinked ${appmakerProfile} -> ${harnessProfile} (login persists across sandbox substitution)`);
|
|
309
|
+
} catch (e) {
|
|
310
|
+
console.warn(` WARNING: failed to symlink AppMaker profile: ${e.message}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// AppMaker VMs also need the twitter-harness MCP server (browser-harness/server.py)
|
|
315
|
+
// to drive port 9222, not its default 9555. That's a SECOND path the env file alone
|
|
316
|
+
// doesn't cover, because the MCP server is spawned by Claude as a subprocess with
|
|
317
|
+
// an env block taken from the MCP config file (--strict-mcp-config replaces the
|
|
318
|
+
// inherited env, so a parent BH_PORT export wouldn't reach it). So we patch the
|
|
319
|
+
// MCP config in-place to bake BH_PORT=9222 into its env block.
|
|
320
|
+
// Idempotent: parses the JSON, sets env.BH_PORT, rewrites. Safe to re-run.
|
|
321
|
+
function applyAppMakerMcpConfigOverrides() {
|
|
322
|
+
const cfgPath = path.join(HOME, '.claude', 'browser-agent-configs', 'twitter-harness-mcp.json');
|
|
323
|
+
if (!fs.existsSync(cfgPath)) {
|
|
324
|
+
console.log(` AppMaker MCP override: ${cfgPath} not found, skipping (will be picked up next run)`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
let cfg;
|
|
328
|
+
try {
|
|
329
|
+
cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
330
|
+
} catch (e) {
|
|
331
|
+
console.warn(` AppMaker MCP override: failed to parse ${cfgPath}: ${e.message}`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const srv = cfg?.mcpServers?.['twitter-harness'];
|
|
335
|
+
if (!srv) {
|
|
336
|
+
console.warn(` AppMaker MCP override: ${cfgPath} has no mcpServers.twitter-harness entry`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (!srv.env || typeof srv.env !== 'object') srv.env = {};
|
|
340
|
+
if (srv.env.BH_PORT === '9222') {
|
|
341
|
+
console.log(` AppMaker MCP override: BH_PORT=9222 already set in ${cfgPath}`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
srv.env.BH_PORT = '9222';
|
|
345
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
346
|
+
console.log(` AppMaker MCP override: set BH_PORT=9222 in ${cfgPath}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Provision the browser-harness toolchain that backs the twitter-harness MCP:
|
|
350
|
+
// 1. install uv (Astral) if missing
|
|
351
|
+
// 2. git-clone browser-use/browser-harness
|
|
352
|
+
// 3. uv tool install -e . (provides the `browser-harness` CLI)
|
|
353
|
+
// 4. ensure `mcp` Python package is importable for server.py
|
|
354
|
+
// 5. copy our shipped server.py into ~/.claude/mcp-servers/browser-harness/
|
|
355
|
+
// All steps are idempotent.
|
|
356
|
+
//
|
|
357
|
+
// AppMaker VMs: the toolchain is STILL needed (the MCP server.py is what
|
|
358
|
+
// Claude invokes during Phase 1's tweet scan, and it requires uv + mcp +
|
|
359
|
+
// browser-harness CLI to run). The AppMaker-specific deltas are:
|
|
360
|
+
// (a) writeAppMakerEnvFile() points TWITTER_CDP_URL at 9222 for posting
|
|
361
|
+
// (b) applyAppMakerMcpConfigOverrides() injects BH_PORT=9222 so server.py
|
|
362
|
+
// drives the AppMaker Chromium instead of trying to launch its own
|
|
363
|
+
// Chrome on 9555. server.py's ensure_chrome() short-circuits when CDP
|
|
364
|
+
// is already alive on PORT, so no double-Chrome ever spawns.
|
|
365
|
+
// Previously we early-returned here on AppMaker, which left the VM without
|
|
366
|
+
// uv installed and broke Phase 1's Claude scan (the MCP server's `command:
|
|
367
|
+
// /root/.local/bin/uv` resolved to ENOENT, Claude got no tools, returned an
|
|
368
|
+
// empty envelope).
|
|
369
|
+
|
|
370
|
+
function installBrowserHarness() {
|
|
371
|
+
const onAppMaker = vmModeEnabled();
|
|
372
|
+
if (onAppMaker) {
|
|
373
|
+
console.log(' AppMaker VM detected -> installing harness toolchain (deps); MCP will be pointed at port 9222');
|
|
374
|
+
writeAppMakerEnvFile();
|
|
375
|
+
// scripts/run_claude.sh uses `uuidgen` for session IDs on AUP-retry. The
|
|
376
|
+
// base image ships libuuid1 (shared lib) but not the CLI tool — the
|
|
377
|
+
// package is `uuid-runtime`. Without it, run_claude.sh's session_id
|
|
378
|
+
// generation falls back to empty string and claude --session-id breaks.
|
|
379
|
+
console.log(' installing uuid-runtime (uuidgen) for run_claude.sh...');
|
|
380
|
+
spawnSync('bash', ['-lc', 'command -v uuidgen >/dev/null 2>&1 || DEBIAN_FRONTEND=noninteractive apt-get install -y -qq uuid-runtime'], { stdio: 'inherit' });
|
|
381
|
+
linkAppMakerHarnessProfile();
|
|
382
|
+
}
|
|
383
|
+
console.log(' setting up browser-harness (twitter-harness MCP backend)...');
|
|
384
|
+
|
|
385
|
+
// Step 1: uv. Try the official installer first; fall back to pip.
|
|
386
|
+
let uvBin = findUvBin();
|
|
387
|
+
if (!uvBin) {
|
|
388
|
+
console.log(' uv not found -> installing via Astral installer');
|
|
389
|
+
const sh = spawnSync('bash', ['-lc', 'curl -LsSf https://astral.sh/uv/install.sh | sh'], { stdio: 'inherit' });
|
|
390
|
+
if (sh.status !== 0) {
|
|
391
|
+
console.log(' Astral installer failed; falling back to pip3 install uv');
|
|
392
|
+
let pip = spawnSync('pip3', ['install', '-q', 'uv'], { stdio: 'inherit' });
|
|
393
|
+
if (pip.status !== 0) {
|
|
394
|
+
pip = spawnSync('pip3', ['install', '-q', 'uv', '--break-system-packages'], { stdio: 'inherit' });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
uvBin = findUvBin();
|
|
398
|
+
}
|
|
399
|
+
if (!uvBin) {
|
|
400
|
+
console.warn(' WARNING: uv install failed; twitter-harness MCP server.py will not start.');
|
|
401
|
+
console.warn(' Install manually: curl -LsSf https://astral.sh/uv/install.sh | sh');
|
|
402
|
+
} else {
|
|
403
|
+
console.log(` uv -> ${uvBin}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Step 2 + 3: clone + `uv tool install -e .` browser-harness.
|
|
407
|
+
//
|
|
408
|
+
// PINNED to a known-good upstream commit instead of tracking origin/HEAD.
|
|
409
|
+
// The installer used to fetch+reset --hard to HEAD on every run, so any
|
|
410
|
+
// upstream change shipped to users untested (this is how the two-blank-tab
|
|
411
|
+
// regression in upstream daemon.py attach behavior could reach users). Our
|
|
412
|
+
// launch-at-real-URL fix in server.py/twitter-backend.sh neutralizes that
|
|
413
|
+
// class of bug regardless, but pinning stops surprise upstream drift. Bump
|
|
414
|
+
// BROWSER_HARNESS_PIN deliberately after validating a newer upstream against
|
|
415
|
+
// the shipped server.py contract.
|
|
416
|
+
const BROWSER_HARNESS_PIN = '6d20866664ea3d9691b27bbf64f42ae097437dc3';
|
|
417
|
+
const harnessDir = path.join(HOME, 'Developer', 'browser-harness');
|
|
418
|
+
const pinHarness = () => {
|
|
419
|
+
// Fetch the exact pinned commit (GitHub serves arbitrary SHAs) and hard-
|
|
420
|
+
// reset onto it. Works for a fresh clone and an existing checkout alike.
|
|
421
|
+
const fetch = spawnSync('git', ['-C', harnessDir, 'fetch', '--depth', '1', 'origin', BROWSER_HARNESS_PIN], { stdio: 'inherit' });
|
|
422
|
+
if (fetch.status !== 0) {
|
|
423
|
+
console.warn(` WARNING: could not fetch pinned browser-harness commit ${BROWSER_HARNESS_PIN.slice(0, 9)}; using existing checkout.`);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const reset = spawnSync('git', ['-C', harnessDir, 'reset', '--hard', 'FETCH_HEAD'], { stdio: 'inherit' });
|
|
427
|
+
if (reset.status !== 0) {
|
|
428
|
+
console.warn(' WARNING: could not reset browser-harness clone to pinned commit; using existing checkout.');
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
if (!fs.existsSync(harnessDir)) {
|
|
432
|
+
fs.mkdirSync(path.dirname(harnessDir), { recursive: true });
|
|
433
|
+
console.log(' cloning browser-harness from GitHub...');
|
|
434
|
+
const clone = spawnSync('git', ['clone', '--depth', '1', 'https://github.com/browser-use/browser-harness', harnessDir], { stdio: 'inherit' });
|
|
435
|
+
if (clone.status !== 0) {
|
|
436
|
+
console.warn(' WARNING: git clone failed; twitter-harness will not work until you clone manually.');
|
|
437
|
+
} else {
|
|
438
|
+
console.log(` pinning browser-harness to ${BROWSER_HARNESS_PIN.slice(0, 9)}...`);
|
|
439
|
+
pinHarness();
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
console.log(` browser-harness clone exists -> ${harnessDir}; pinning to ${BROWSER_HARNESS_PIN.slice(0, 9)}...`);
|
|
443
|
+
pinHarness();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (uvBin && fs.existsSync(harnessDir)) {
|
|
447
|
+
console.log(' installing browser-harness CLI via uv tool...');
|
|
448
|
+
// --force so a refreshed source / changed entry point is reinstalled even
|
|
449
|
+
// when the tool is already present (a plain re-install is otherwise a no-op).
|
|
450
|
+
const install = spawnSync(uvBin, ['tool', 'install', '--force', '-e', harnessDir], { stdio: 'inherit' });
|
|
451
|
+
if (install.status !== 0) {
|
|
452
|
+
console.warn(' WARNING: `uv tool install -e .` failed; check the output above.');
|
|
453
|
+
}
|
|
454
|
+
// The harness daemon caches imported code in a long-running process; drop it
|
|
455
|
+
// so the next bh_run loads the freshly-installed CLI instead of stale code.
|
|
456
|
+
const harnessBin = path.join(HOME, '.local', 'bin', 'browser-harness');
|
|
457
|
+
if (fs.existsSync(harnessBin)) {
|
|
458
|
+
spawnSync(harnessBin, ['--reload'], { stdio: 'inherit' });
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Contract check: server.py pipes the script to browser-harness via stdin.
|
|
462
|
+
// Upstream supports two banner shapes — older builds advertise `-c <script>`
|
|
463
|
+
// and newer builds advertise the `<<'PY' ... PY` heredoc form. Either is
|
|
464
|
+
// fine for our use case (we pass the script via stdin, which both accept).
|
|
465
|
+
// Fail loudly if the installed binary advertises NEITHER, which usually
|
|
466
|
+
// means an offline/partial clone left a broken CLI that will silently make
|
|
467
|
+
// every bh_run look like "CDP not connected".
|
|
468
|
+
if (fs.existsSync(harnessBin)) {
|
|
469
|
+
const probe = spawnSync(harnessBin, [], { stdio: 'pipe', encoding: 'utf8', timeout: 15000 });
|
|
470
|
+
const usage = `${probe.stdout || ''}${probe.stderr || ''}`;
|
|
471
|
+
const supportsDashC = /\b-c\b/.test(usage);
|
|
472
|
+
const supportsStdin = /<<'PY'|<<"PY"|<<PY\b/.test(usage);
|
|
473
|
+
if (!supportsDashC && !supportsStdin) {
|
|
474
|
+
console.error(' ERROR: installed browser-harness CLI advertises neither `-c` nor a stdin heredoc.');
|
|
475
|
+
console.error(' This usually means a partial/corrupted install. The twitter-harness MCP will');
|
|
476
|
+
console.error(' return a usage banner / "CDP not connected" on every call.');
|
|
477
|
+
console.error(` Fix: rm -rf ${harnessDir} && re-run \`social-autoposter init\` while online,`);
|
|
478
|
+
console.error(' or manually: git clone https://github.com/browser-use/browser-harness ' + harnessDir +
|
|
479
|
+
' && ' + uvBin + ' tool install --force -e ' + harnessDir);
|
|
480
|
+
} else {
|
|
481
|
+
const shape = supportsStdin ? 'stdin heredoc' : '-c flag';
|
|
482
|
+
console.log(` browser-harness CLI verified (${shape}).`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Step 4: ensure mcp Python package available (server.py uses `from mcp.server.fastmcp ...`).
|
|
488
|
+
// server.py is shebanged through `uv run --with mcp ...` so this is belt-and-suspenders;
|
|
489
|
+
// we install it into the S4L_PYTHON interpreter (the same Homebrew python the MCP
|
|
490
|
+
// server is configured to use), NOT bare pip3 which targets the Xcode CLT system python.
|
|
491
|
+
const harnessPython = findPythonBin();
|
|
492
|
+
console.log(` ensuring mcp>=1.0.0 Python package is importable (${harnessPython})...`);
|
|
493
|
+
const pip = pipInstall(harnessPython, ['-q', 'mcp>=1.0.0']);
|
|
494
|
+
if (pip.status !== 0) {
|
|
495
|
+
console.warn(' WARNING: could not install mcp Python package; server.py still runs via `uv run --with mcp`.');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Step 5: copy our shipped server.py into the canonical install location.
|
|
499
|
+
const srcServer = path.join(PKG_ROOT, 'mcp-servers', 'browser-harness', 'server.py');
|
|
500
|
+
const destServer = path.join(HOME, '.claude', 'mcp-servers', 'browser-harness', 'server.py');
|
|
501
|
+
if (fs.existsSync(srcServer)) {
|
|
502
|
+
fs.mkdirSync(path.dirname(destServer), { recursive: true });
|
|
503
|
+
fs.copyFileSync(srcServer, destServer);
|
|
504
|
+
try { fs.chmodSync(destServer, 0o755); } catch {}
|
|
505
|
+
console.log(` server.py -> ${destServer}`);
|
|
506
|
+
} else {
|
|
507
|
+
console.warn(` WARNING: package missing mcp-servers/browser-harness/server.py (${srcServer})`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Register the three browser-agent MCP servers with Claude so they show up
|
|
512
|
+
// under user scope (writes to ~/.claude.json). Idempotent: parses the output
|
|
513
|
+
// of `claude mcp list` and only calls `add-json` for missing entries.
|
|
514
|
+
// If the `claude` CLI is not on PATH, prints manual instructions and returns.
|
|
515
|
+
function registerBrowserAgentMcpServers() {
|
|
516
|
+
const configDir = path.join(HOME, '.claude', 'browser-agent-configs');
|
|
517
|
+
// twitter-agent retired 2026-05-19, replaced by twitter-harness (CDP-driven
|
|
518
|
+
// real Chrome on port 9555 via the browser-harness MCP server).
|
|
519
|
+
const servers = [
|
|
520
|
+
{ name: 'reddit-agent', file: path.join(configDir, 'reddit-agent-mcp.json') },
|
|
521
|
+
{ name: 'linkedin-agent', file: path.join(configDir, 'linkedin-agent-mcp.json') },
|
|
522
|
+
{ name: 'twitter-harness', file: path.join(configDir, 'twitter-harness-mcp.json') },
|
|
523
|
+
];
|
|
524
|
+
|
|
525
|
+
const claudeBin = spawnSync('claude', ['--version'], { stdio: 'pipe' });
|
|
526
|
+
if (claudeBin.status !== 0) {
|
|
527
|
+
console.log(' claude CLI not on PATH; skipping MCP registration.');
|
|
528
|
+
console.log(' Once Claude Code is installed, register manually with:');
|
|
529
|
+
for (const s of servers) {
|
|
530
|
+
console.log(` claude mcp add-json ${s.name} "$(jq -c .mcpServers['\\"'${s.name}'\\"'] ${s.file})"`);
|
|
531
|
+
}
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const list = spawnSync('claude', ['mcp', 'list'], { encoding: 'utf8' });
|
|
536
|
+
const existing = list.status === 0 ? list.stdout : '';
|
|
537
|
+
|
|
538
|
+
let added = 0;
|
|
539
|
+
let skipped = 0;
|
|
540
|
+
for (const s of servers) {
|
|
541
|
+
if (!fs.existsSync(s.file)) {
|
|
542
|
+
console.warn(` MCP config missing: ${s.file}`);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
// `claude mcp list` prints one server per line starting with the name.
|
|
546
|
+
// Use a word-boundary check so e.g. reddit-agent does not false-match linkedin-agent.
|
|
547
|
+
const re = new RegExp(`(^|\\s)${s.name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}(:|\\s|$)`, 'm');
|
|
548
|
+
if (re.test(existing)) {
|
|
549
|
+
skipped++;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
const tpl = JSON.parse(fs.readFileSync(s.file, 'utf8'));
|
|
553
|
+
const stanza = tpl.mcpServers && tpl.mcpServers[s.name];
|
|
554
|
+
if (!stanza) {
|
|
555
|
+
console.warn(` ${s.file} has no mcpServers.${s.name} stanza; skipping`);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const r = spawnSync('claude', ['mcp', 'add-json', s.name, JSON.stringify(stanza)], { stdio: 'pipe', encoding: 'utf8' });
|
|
559
|
+
if (r.status === 0) {
|
|
560
|
+
added++;
|
|
561
|
+
} else {
|
|
562
|
+
console.warn(` claude mcp add-json ${s.name} failed: ${(r.stderr || r.stdout || '').trim()}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
console.log(` MCP servers registered with Claude (added ${added}, already present ${skipped})`);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function generatePlists() {
|
|
569
|
+
const nodeBin = path.dirname(process.execPath);
|
|
570
|
+
const jobs = [
|
|
571
|
+
{
|
|
572
|
+
file: 'com.m13v.social-stats.plist',
|
|
573
|
+
label: 'com.m13v.social-stats',
|
|
574
|
+
script: `${DEST}/skill/stats.sh`,
|
|
575
|
+
interval: 21600,
|
|
576
|
+
runAtLoad: false,
|
|
577
|
+
stdoutLog: `${DEST}/skill/logs/launchd-stats-stdout.log`,
|
|
578
|
+
stderrLog: `${DEST}/skill/logs/launchd-stats-stderr.log`,
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
// Daily self-updater. Pulls + installs the latest published release so a
|
|
582
|
+
// hands-free / headless install never drifts stale. The script refuses to
|
|
583
|
+
// touch a .git dev checkout, so it is a safe no-op on a source box.
|
|
584
|
+
file: 'com.m13v.social-autoposter-update.plist',
|
|
585
|
+
label: 'com.m13v.social-autoposter-update',
|
|
586
|
+
script: `${DEST}/skill/social-autoposter-update.sh`,
|
|
587
|
+
interval: 86400,
|
|
588
|
+
runAtLoad: true,
|
|
589
|
+
stdoutLog: `${DEST}/skill/logs/launchd-self-update-stdout.log`,
|
|
590
|
+
stderrLog: `${DEST}/skill/logs/launchd-self-update-stderr.log`,
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
// On-screen overlay watcher supervisor. The overlay (harness status banner)
|
|
594
|
+
// only renders WHILE harness_overlay.py watch runs. The supervisor is
|
|
595
|
+
// idempotent (pgrep guard), so a 60s StartInterval
|
|
596
|
+
// is a no-op while the watcher is up and re-spawns it within a minute if it
|
|
597
|
+
// ever dies. RunAtLoad starts it right after install. This is what makes the
|
|
598
|
+
// overlay appear on headless / remote installs (Lane A); the MCP covers the
|
|
599
|
+
// pure-.mcpb lane by calling the same script on draft_cycle / autopilot.
|
|
600
|
+
file: 'com.m13v.social-overlay-watch.plist',
|
|
601
|
+
label: 'com.m13v.social-overlay-watch',
|
|
602
|
+
script: `${DEST}/skill/run-overlay-watch.sh`,
|
|
603
|
+
interval: 60,
|
|
604
|
+
runAtLoad: true,
|
|
605
|
+
stdoutLog: `${DEST}/skill/logs/launchd-overlay-watch-stdout.log`,
|
|
606
|
+
stderrLog: `${DEST}/skill/logs/launchd-overlay-watch-stderr.log`,
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
// Read-only LinkedIn presence lane. Uses the same linkedin-harness Chrome
|
|
610
|
+
// and browser locks as the main LinkedIn pipelines, but only views first-
|
|
611
|
+
// party surfaces and performs bounded scroll passes.
|
|
612
|
+
file: 'com.m13v.social-linkedin-presence.plist',
|
|
613
|
+
label: 'com.m13v.social-linkedin-presence',
|
|
614
|
+
script: `${DEST}/skill/linkedin-presence.sh`,
|
|
615
|
+
interval: 7200,
|
|
616
|
+
runAtLoad: false,
|
|
617
|
+
stdoutLog: `${DEST}/skill/logs/launchd-linkedin-presence-stdout.log`,
|
|
618
|
+
stderrLog: `${DEST}/skill/logs/launchd-linkedin-presence-stderr.log`,
|
|
619
|
+
},
|
|
620
|
+
];
|
|
621
|
+
|
|
622
|
+
const driver = scheduler.driverFor();
|
|
623
|
+
const env = driver.defaultEnv({ home: HOME, nodeBin });
|
|
624
|
+
const outDir = path.join(DEST, 'launchd');
|
|
625
|
+
driver.generate({ jobs, outDir, env });
|
|
626
|
+
console.log(` generated launchd units at ${outDir}`);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function init() {
|
|
630
|
+
console.log('Setting up social-autoposter in', DEST);
|
|
631
|
+
fs.mkdirSync(DEST, { recursive: true });
|
|
632
|
+
|
|
633
|
+
// Copy all package files
|
|
634
|
+
for (const f of COPY_TARGETS) {
|
|
635
|
+
const src = path.join(PKG_ROOT, f);
|
|
636
|
+
const dest = path.join(DEST, f);
|
|
637
|
+
if (!fs.existsSync(src)) continue;
|
|
638
|
+
const stat = fs.statSync(src);
|
|
639
|
+
if (stat.isDirectory()) {
|
|
640
|
+
copyDir(src, dest);
|
|
641
|
+
} else {
|
|
642
|
+
fs.copyFileSync(src, dest);
|
|
643
|
+
}
|
|
644
|
+
console.log(' copied', f);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Generate launchd plists with user's actual HOME
|
|
648
|
+
generatePlists();
|
|
649
|
+
|
|
650
|
+
// Provision the browser-harness toolchain BEFORE writing harness configs so
|
|
651
|
+
// findUvBin() picks up a freshly-installed uv on first run.
|
|
652
|
+
installBrowserHarness();
|
|
653
|
+
// Install browser agent MCP configs + profile dirs (skips existing files)
|
|
654
|
+
installBrowserAgentConfigs();
|
|
655
|
+
// On AppMaker VMs, patch the twitter-harness MCP config so its server.py
|
|
656
|
+
// drives port 9222 (AppMaker Chromium) instead of the default 9555.
|
|
657
|
+
if (vmModeEnabled()) applyAppMakerMcpConfigOverrides();
|
|
658
|
+
// Register those MCP servers with Claude so they show up in `claude mcp list`.
|
|
659
|
+
registerBrowserAgentMcpServers();
|
|
660
|
+
|
|
661
|
+
// config.json — only if it doesn't exist
|
|
662
|
+
const configDest = path.join(DEST, 'config.json');
|
|
663
|
+
if (!fs.existsSync(configDest)) {
|
|
664
|
+
fs.copyFileSync(path.join(PKG_ROOT, 'config.example.json'), configDest);
|
|
665
|
+
console.log(' created config.json from template');
|
|
666
|
+
} else {
|
|
667
|
+
console.log(' config.json exists — skipping');
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// No .env is created. X/Twitter and the rest of the pipeline run with zero
|
|
671
|
+
// keys — state syncs through the s4l.ai HTTP API and the browser session
|
|
672
|
+
// lives in the harness Chrome profile. Optional integrations read their keys
|
|
673
|
+
// straight from the environment when set (MOLTBOOK_API_KEY for Moltbook,
|
|
674
|
+
// AUTOPOSTER_API_KEY only if your s4l.ai install uses a bearer token); every
|
|
675
|
+
// script guards `.env` with `[ -f .env ]`, so its absence is a no-op.
|
|
676
|
+
|
|
677
|
+
installPythonDeps();
|
|
678
|
+
removeLegacyEngagementStylesSidecar();
|
|
679
|
+
installMcp();
|
|
680
|
+
|
|
681
|
+
// Remove stale skill/SKILL.md if it exists (SKILL.md lives at repo root only)
|
|
682
|
+
const skillMd = path.join(DEST, 'skill', 'SKILL.md');
|
|
683
|
+
try { fs.rmSync(skillMd, { force: true }); } catch {}
|
|
684
|
+
|
|
685
|
+
// Skill symlinks — point to repo root so Claude loads SKILL.md directly
|
|
686
|
+
const skillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
687
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
688
|
+
linkOrRelink(DEST, path.join(skillsDir, 'social-autoposter'));
|
|
689
|
+
console.log(' ~/.claude/skills/social-autoposter ->', DEST);
|
|
690
|
+
linkOrRelink(path.join(DEST, 'setup'), path.join(skillsDir, 'social-autoposter-setup'));
|
|
691
|
+
console.log(' ~/.claude/skills/social-autoposter-setup ->', path.join(DEST, 'setup'));
|
|
692
|
+
|
|
693
|
+
console.log('');
|
|
694
|
+
console.log('Done! Next steps:');
|
|
695
|
+
console.log(' 1. Fully quit and relaunch Claude so the MCP loads');
|
|
696
|
+
console.log(' 2. Tell your Claude agent: "set me up on social-autoposter plugin end to end"');
|
|
697
|
+
console.log(' The agent will configure the product, connect X, seed topics, and verify a draft cycle');
|
|
698
|
+
console.log(' 3. Posts and all pipeline state sync via the s4l.ai HTTP API (no Postgres required)');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function update() {
|
|
702
|
+
if (!fs.existsSync(DEST)) {
|
|
703
|
+
console.error('Not installed. Run: npx social-autoposter init');
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
console.log('Updating social-autoposter...');
|
|
708
|
+
|
|
709
|
+
for (const f of COPY_TARGETS) {
|
|
710
|
+
if (USER_FILES.has(f)) {
|
|
711
|
+
console.log(' skipping', f, '(user file)');
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
const src = path.join(PKG_ROOT, f);
|
|
715
|
+
const dest = path.join(DEST, f);
|
|
716
|
+
if (!fs.existsSync(src)) continue;
|
|
717
|
+
const stat = fs.statSync(src);
|
|
718
|
+
if (stat.isDirectory()) {
|
|
719
|
+
copyDir(src, dest);
|
|
720
|
+
} else {
|
|
721
|
+
fs.copyFileSync(src, dest);
|
|
722
|
+
}
|
|
723
|
+
console.log(' updated', f);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Regenerate launchd plists with correct paths
|
|
727
|
+
generatePlists();
|
|
728
|
+
|
|
729
|
+
// Provision browser-harness (uv + clone + uv tool install + mcp pkg + server.py).
|
|
730
|
+
// Idempotent: skips steps that are already done.
|
|
731
|
+
installBrowserHarness();
|
|
732
|
+
// Top up browser agent configs (won't overwrite user customizations)
|
|
733
|
+
installBrowserAgentConfigs();
|
|
734
|
+
// On AppMaker VMs, patch the twitter-harness MCP config so its server.py
|
|
735
|
+
// drives port 9222 (AppMaker Chromium) instead of the default 9555.
|
|
736
|
+
if (vmModeEnabled()) applyAppMakerMcpConfigOverrides();
|
|
737
|
+
// Register any newly added MCP servers with Claude (idempotent).
|
|
738
|
+
registerBrowserAgentMcpServers();
|
|
739
|
+
|
|
740
|
+
// Refresh Python deps every update so version-bumps land on existing installs
|
|
741
|
+
// and the candidate-style sidecar gets merged (preserves VM-side candidates).
|
|
742
|
+
installPythonDeps();
|
|
743
|
+
removeLegacyEngagementStylesSidecar();
|
|
744
|
+
installMcp();
|
|
745
|
+
|
|
746
|
+
// Remove stale skill/SKILL.md if it exists (SKILL.md lives at repo root only)
|
|
747
|
+
const skillMd = path.join(DEST, 'skill', 'SKILL.md');
|
|
748
|
+
try { fs.rmSync(skillMd, { force: true }); } catch {}
|
|
749
|
+
|
|
750
|
+
// Re-symlink skills — point to repo root so Claude loads SKILL.md directly
|
|
751
|
+
const skillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
752
|
+
try {
|
|
753
|
+
linkOrRelink(DEST, path.join(skillsDir, 'social-autoposter'));
|
|
754
|
+
console.log(' re-linked ~/.claude/skills/social-autoposter');
|
|
755
|
+
} catch {}
|
|
756
|
+
try {
|
|
757
|
+
linkOrRelink(path.join(DEST, 'setup'), path.join(skillsDir, 'social-autoposter-setup'));
|
|
758
|
+
console.log(' re-linked ~/.claude/skills/social-autoposter-setup');
|
|
759
|
+
} catch {}
|
|
760
|
+
|
|
761
|
+
console.log('');
|
|
762
|
+
console.log('Update complete. config.json was preserved.');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Install Python deps from requirements.txt (preferred) or fall back to the
|
|
766
|
+
// hardcoded list. Idempotent — pip3 install is a no-op when the package is
|
|
767
|
+
// already at the requested version. Playwright also needs the Chromium
|
|
768
|
+
// browser binary; we run `playwright install chromium` after the pip install.
|
|
769
|
+
function installPythonDeps() {
|
|
770
|
+
const reqPath = path.join(PKG_ROOT, 'requirements.txt');
|
|
771
|
+
const args = fs.existsSync(reqPath)
|
|
772
|
+
? ['-r', reqPath, '-q']
|
|
773
|
+
: ['-q', 'playwright'];
|
|
774
|
+
// Install into the SAME interpreter the MCP server runs (S4L_PYTHON =
|
|
775
|
+
// Homebrew python), NOT bare pip3 which on macOS targets the Xcode CLT system
|
|
776
|
+
// python — deps installed there are invisible to the scripts at runtime.
|
|
777
|
+
// pipInstall() also gates --break-system-packages on pip>=23 so it doesn't
|
|
778
|
+
// hard-fail against the ancient system pip.
|
|
779
|
+
const pythonBin = findPythonBin();
|
|
780
|
+
console.log(` installing Python deps (playwright, ...) into ${pythonBin}`);
|
|
781
|
+
const r = pipInstall(pythonBin, args);
|
|
782
|
+
if (r.status !== 0) {
|
|
783
|
+
console.warn(' WARNING: pip install failed — run manually:');
|
|
784
|
+
console.warn(` ${pythonBin} -m pip install ${args.join(' ')} --break-system-packages`);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
// Playwright needs its browser binary downloaded separately. Chromium
|
|
788
|
+
// is the only engine the repo uses today; skip Firefox/WebKit.
|
|
789
|
+
console.log(' installing Playwright Chromium binary (one-time, ~150MB)...');
|
|
790
|
+
const pw = spawnSync(pythonBin, ['-m', 'playwright', 'install', 'chromium'], { stdio: 'inherit' });
|
|
791
|
+
if (pw.status !== 0) {
|
|
792
|
+
console.warn(' WARNING: playwright install chromium failed — run manually:');
|
|
793
|
+
console.warn(` ${pythonBin} -m playwright install chromium`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Set up the social-autoposter MCP server (the X/Twitter draft/autopilot/stats
|
|
798
|
+
// surface for Claude Desktop + Claude Code). The package ships a prebuilt
|
|
799
|
+
// mcp/dist/, so we only install the runtime deps (@modelcontextprotocol/sdk +
|
|
800
|
+
// zod) and register the server into both clients. REPO_DIR auto-resolves to
|
|
801
|
+
// ~/social-autoposter (mcp/../..) so no env wiring is needed beyond what
|
|
802
|
+
// install.mjs pins. Idempotent; safe on both init and update.
|
|
803
|
+
function installMcp() {
|
|
804
|
+
const mcpDest = path.join(DEST, 'mcp');
|
|
805
|
+
if (!fs.existsSync(path.join(mcpDest, 'package.json'))) {
|
|
806
|
+
console.warn(' WARNING: mcp/ missing from install — skipping MCP setup');
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
// Stamp the REAL shipped version (this npm package's version) into the MCP so
|
|
810
|
+
// it can report itself accurately at runtime. The top-level package.json is
|
|
811
|
+
// NOT copied into the install, so without this the MCP can't see its true
|
|
812
|
+
// version. mcp/src/version.ts reads dist/version.json first. Refreshed on
|
|
813
|
+
// every init/update.
|
|
814
|
+
try {
|
|
815
|
+
const pkgVersion = require('../package.json').version;
|
|
816
|
+
const distDir = path.join(mcpDest, 'dist');
|
|
817
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
818
|
+
fs.writeFileSync(
|
|
819
|
+
path.join(distDir, 'version.json'),
|
|
820
|
+
JSON.stringify({ version: pkgVersion, installedAt: new Date().toISOString() }, null, 2)
|
|
821
|
+
);
|
|
822
|
+
console.log(' stamped MCP version', pkgVersion);
|
|
823
|
+
} catch (e) {
|
|
824
|
+
console.warn(' WARNING: could not stamp MCP version:', e && e.message);
|
|
825
|
+
}
|
|
826
|
+
console.log(' installing MCP runtime deps (npm install --omit=dev in mcp/)');
|
|
827
|
+
const npmRes = spawnSync('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], {
|
|
828
|
+
cwd: mcpDest,
|
|
829
|
+
stdio: 'inherit',
|
|
830
|
+
});
|
|
831
|
+
if (npmRes.status !== 0) {
|
|
832
|
+
console.warn(' WARNING: npm install in mcp/ failed — run manually:');
|
|
833
|
+
console.warn(' (cd ~/social-autoposter/mcp && npm install --omit=dev)');
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
console.log(' registering social-autoposter MCP with Claude Desktop + Claude Code');
|
|
837
|
+
const reg = spawnSync('node', ['install.mjs'], { cwd: mcpDest, stdio: 'inherit' });
|
|
838
|
+
if (reg.status !== 0) {
|
|
839
|
+
console.warn(' WARNING: MCP client registration failed — run manually:');
|
|
840
|
+
console.warn(' (cd ~/social-autoposter/mcp && node install.mjs)');
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Sweep the legacy candidate-style sidecar JSON + lock file off every install.
|
|
845
|
+
// The taxonomy lives in Postgres `engagement_styles_registry` now (single
|
|
846
|
+
// source of truth for all installs, no per-machine JSON drift); see
|
|
847
|
+
// scripts/migrate_engagement_styles_to_db.py for the cutover. We keep this
|
|
848
|
+
// helper around for a release or two so existing installs auto-clean the
|
|
849
|
+
// dead files on next `init` / `update`, then it can go.
|
|
850
|
+
function removeLegacyEngagementStylesSidecar() {
|
|
851
|
+
const targets = [
|
|
852
|
+
path.join(DEST, 'scripts', 'engagement_styles_extra.json'),
|
|
853
|
+
path.join(DEST, 'scripts', 'engagement_styles_extra.json.lock'),
|
|
854
|
+
];
|
|
855
|
+
for (const p of targets) {
|
|
856
|
+
if (fs.existsSync(p)) {
|
|
857
|
+
try {
|
|
858
|
+
fs.rmSync(p, { force: true });
|
|
859
|
+
console.log(` removed legacy ${path.relative(DEST, p)} (registry is now in Postgres)`);
|
|
860
|
+
} catch (e) {
|
|
861
|
+
console.warn(` WARNING: could not remove ${p}: ${e.message}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// `doctor` is a structured diagnostic engine shared with MCP onboarding.
|
|
868
|
+
// Human-readable output remains the default; --json gives setup tools and CI a
|
|
869
|
+
// stable machine-readable report. --phase pre_connect treats the not-yet-created
|
|
870
|
+
// X session/cookie artifacts as expected, while full verifies the completed
|
|
871
|
+
// environment after connect_x.
|
|
872
|
+
function doctor() {
|
|
873
|
+
const args = process.argv.slice(3);
|
|
874
|
+
const json = args.includes('--json');
|
|
875
|
+
const phaseArg = args.find((arg) => arg.startsWith('--phase='));
|
|
876
|
+
const phaseIndex = args.indexOf('--phase');
|
|
877
|
+
const phase =
|
|
878
|
+
(phaseArg && phaseArg.slice('--phase='.length)) ||
|
|
879
|
+
(phaseIndex >= 0 ? args[phaseIndex + 1] : null) ||
|
|
880
|
+
'full';
|
|
881
|
+
if (!['pre_connect', 'full'].includes(phase)) {
|
|
882
|
+
console.error("doctor: --phase must be 'pre_connect' or 'full'");
|
|
883
|
+
process.exit(2);
|
|
884
|
+
}
|
|
885
|
+
const report = runDoctorSync({
|
|
886
|
+
phase,
|
|
887
|
+
home: HOME,
|
|
888
|
+
repoDir: fs.existsSync(DEST) ? DEST : PKG_ROOT,
|
|
889
|
+
python: findPythonBin(),
|
|
890
|
+
});
|
|
891
|
+
// Doctor runs are durable even when invoked directly from the CLI. MCP uses
|
|
892
|
+
// this same ledger, so a later onboarding session can show the historical run.
|
|
893
|
+
recordDoctorReport(report);
|
|
894
|
+
console.log(json ? JSON.stringify(report, null, 2) : formatDoctorReport(report));
|
|
895
|
+
if (!report.ok) process.exitCode = 1;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Provision the owned Python/Chromium runtime from the terminal. This is the
|
|
899
|
+
// panel-free path: it runs the EXACT same provisioning logic the panel's
|
|
900
|
+
// "Install runtime" button and the install_runtime MCP tool use (mcp/src/
|
|
901
|
+
// runtime.ts -> dist/runtime.js), via the thin ESM wrapper mcp/install-runtime.mjs.
|
|
902
|
+
// Use it when the UI panel can't render (Claude Code/Cowork), on a bare VM, or
|
|
903
|
+
// when an agent wants to install head-less. Idempotent: re-running repairs.
|
|
904
|
+
function installRuntime() {
|
|
905
|
+
const wrapper = path.join(__dirname, '..', 'mcp', 'install-runtime.mjs');
|
|
906
|
+
if (!fs.existsSync(wrapper)) {
|
|
907
|
+
console.error(`Cannot find ${wrapper}. Re-run \`npx social-autoposter update\` to repair the install.`);
|
|
908
|
+
process.exit(1);
|
|
909
|
+
}
|
|
910
|
+
// process.execPath is the Node already running this CLI, so we reuse it
|
|
911
|
+
// rather than hunting for a node on PATH.
|
|
912
|
+
const res = spawnSync(process.execPath, [wrapper], { stdio: 'inherit' });
|
|
913
|
+
process.exit(res.status == null ? 1 : res.status);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Wipe a social-autoposter install back to factory-fresh (test machines /
|
|
917
|
+
// uninstall). Shells out to the bundled scripts/reset-test-machine.sh, which is
|
|
918
|
+
// the single source of truth: it removes the owned state dir (~/.social-
|
|
919
|
+
// autoposter-mcp), packaged Chrome profiles + imported cookies, the browser-
|
|
920
|
+
// harness clone/CLI/server.py, the global npm package, and the MCP registration.
|
|
921
|
+
// DEFAULT IS A DRY RUN (prints what WOULD be removed); pass --yes to apply, and
|
|
922
|
+
// --deep to also remove the shared uv toolchain + Chromium cache. The script's
|
|
923
|
+
// one standard path quits Claude Desktop, wipes, settles, then relaunches
|
|
924
|
+
// Claude Desktop fresh. Forwarding to the shell script keeps npm and .mcpb
|
|
925
|
+
// installs behaving identically.
|
|
926
|
+
function reset() {
|
|
927
|
+
const script = path.join(PKG_ROOT, 'scripts', 'reset-test-machine.sh');
|
|
928
|
+
if (!fs.existsSync(script)) {
|
|
929
|
+
console.error(`Cannot find ${script}. Re-run \`npx social-autoposter update\` to repair the install.`);
|
|
930
|
+
process.exit(1);
|
|
931
|
+
}
|
|
932
|
+
// Forward everything after `reset` (e.g. --yes, --deep) straight to the script.
|
|
933
|
+
const res = spawnSync('bash', [script, ...process.argv.slice(3)], { stdio: 'inherit' });
|
|
934
|
+
process.exit(res.status == null ? 1 : res.status);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const cmd = process.argv[2];
|
|
938
|
+
if (cmd === 'init') {
|
|
939
|
+
init();
|
|
940
|
+
} else if (cmd === 'update') {
|
|
941
|
+
update();
|
|
942
|
+
} else if (cmd === 'doctor') {
|
|
943
|
+
doctor();
|
|
944
|
+
} else if (cmd === 'install-runtime') {
|
|
945
|
+
installRuntime();
|
|
946
|
+
} else if (cmd === 'reset' || cmd === 'uninstall') {
|
|
947
|
+
reset();
|
|
948
|
+
} else if (cmd === 'export-cookies') {
|
|
949
|
+
// Forward to cookie-helper with 'export' + remaining args
|
|
950
|
+
process.argv = [process.argv[0], process.argv[1], 'export', ...process.argv.slice(3)];
|
|
951
|
+
require('./cookie-helper.js');
|
|
952
|
+
} else if (cmd === 'import-cookies') {
|
|
953
|
+
// Forward to cookie-helper with 'import' + remaining args
|
|
954
|
+
process.argv = [process.argv[0], process.argv[1], 'import', ...process.argv.slice(3)];
|
|
955
|
+
require('./cookie-helper.js');
|
|
956
|
+
} else if (!cmd) {
|
|
957
|
+
// The dashboard server (bin/server.js) is a local-only operator tool and is
|
|
958
|
+
// NOT shipped in the published package (it talks directly to Postgres). When
|
|
959
|
+
// it's absent, fall back to usage help instead of crashing on a missing require.
|
|
960
|
+
if (fs.existsSync(path.join(__dirname, 'server.js'))) {
|
|
961
|
+
require('./server.js');
|
|
962
|
+
} else {
|
|
963
|
+
console.log('social-autoposter — automated social posting for Claude agents');
|
|
964
|
+
console.log('');
|
|
965
|
+
console.log('The local dashboard is not part of the published package.');
|
|
966
|
+
console.log('Run `npx social-autoposter init` to set up, then drive it from your Claude agent.');
|
|
967
|
+
}
|
|
968
|
+
} else {
|
|
969
|
+
console.log('social-autoposter — automated social posting for Claude agents');
|
|
970
|
+
console.log('');
|
|
971
|
+
console.log('Usage:');
|
|
972
|
+
console.log(' npx social-autoposter open the dashboard');
|
|
973
|
+
console.log(' npx social-autoposter init first-time setup');
|
|
974
|
+
console.log(' npx social-autoposter update update scripts, preserve config');
|
|
975
|
+
console.log(' npx social-autoposter doctor [--json] [--phase pre_connect|full]');
|
|
976
|
+
console.log(' npx social-autoposter install-runtime provision owned Python + Chromium (panel-free)');
|
|
977
|
+
console.log(' npx social-autoposter export-cookies [dir] export browser cookies');
|
|
978
|
+
console.log(' npx social-autoposter import-cookies [dir] import browser cookies');
|
|
979
|
+
console.log(' npx social-autoposter reset [--yes] [--deep] uninstall/wipe the install (dry-run unless --yes)');
|
|
980
|
+
}
|