@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.
Files changed (326) hide show
  1. package/README.md +143 -0
  2. package/SKILL.md +342 -0
  3. package/bin/cli.js +980 -0
  4. package/bin/cookie-helper.js +315 -0
  5. package/bin/platform.js +59 -0
  6. package/bin/scheduler/index.js +12 -0
  7. package/bin/scheduler/launchd.js +518 -0
  8. package/browser-agent-configs/all-agents-mcp.json +68 -0
  9. package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
  10. package/browser-agent-configs/linkedin-agent.json +17 -0
  11. package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
  12. package/browser-agent-configs/reddit-agent-mcp.json +16 -0
  13. package/browser-agent-configs/reddit-agent.json +17 -0
  14. package/browser-agent-configs/twitter-harness-mcp.json +18 -0
  15. package/config.example.json +45 -0
  16. package/mcp/dist/index.js +4212 -0
  17. package/mcp/dist/onboarding.js +200 -0
  18. package/mcp/dist/panel.html +176 -0
  19. package/mcp/dist/product-link.html +102 -0
  20. package/mcp/dist/repo.js +222 -0
  21. package/mcp/dist/runtime.js +1079 -0
  22. package/mcp/dist/screencast.js +323 -0
  23. package/mcp/dist/setup.js +545 -0
  24. package/mcp/dist/telemetry.js +306 -0
  25. package/mcp/dist/twitterAuth.js +138 -0
  26. package/mcp/dist/version.js +271 -0
  27. package/mcp/dist/version.json +4 -0
  28. package/mcp/install-runtime.mjs +70 -0
  29. package/mcp/install.mjs +169 -0
  30. package/mcp/manifest.json +80 -0
  31. package/mcp/menubar/dashboard_server.py +213 -0
  32. package/mcp/menubar/s4l_card.py +1314 -0
  33. package/mcp/menubar/s4l_log_relay.py +179 -0
  34. package/mcp/menubar/s4l_menubar.py +2439 -0
  35. package/mcp/menubar/s4l_state.py +891 -0
  36. package/mcp/package.json +34 -0
  37. package/mcp/shared/doctor.cjs +437 -0
  38. package/mcp/shared/onboarding-ledger.cjs +324 -0
  39. package/mcp-servers/browser-harness/server.py +968 -0
  40. package/package.json +160 -0
  41. package/requirements.txt +20 -0
  42. package/scripts/_compute_allowlist.py +58 -0
  43. package/scripts/_db_update.py +20 -0
  44. package/scripts/_filt.py +9 -0
  45. package/scripts/_li_notif_match.py +76 -0
  46. package/scripts/_li_notif_orchestrate.py +126 -0
  47. package/scripts/_lock_preempt_test.py +60 -0
  48. package/scripts/_run_icp_precheck.py +57 -0
  49. package/scripts/a16z_pearx_calendar_reminders.py +99 -0
  50. package/scripts/account_resolver.py +141 -0
  51. package/scripts/active_campaigns.py +114 -0
  52. package/scripts/active_users.py +190 -0
  53. package/scripts/amplitude_24h_signups.py +468 -0
  54. package/scripts/amplitude_signups.py +177 -0
  55. package/scripts/apply_onboarding_selections.py +131 -0
  56. package/scripts/audience_pages.py +243 -0
  57. package/scripts/audit_helper.py +120 -0
  58. package/scripts/author_history_block.py +353 -0
  59. package/scripts/autopilot_stall_watch.py +284 -0
  60. package/scripts/backfill_twitter_attempts_topic.py +81 -0
  61. package/scripts/backfill_twitter_log_post_no_id.py +322 -0
  62. package/scripts/bench_dashboard.sh +138 -0
  63. package/scripts/bh_send.py +39 -0
  64. package/scripts/build_persona.py +409 -0
  65. package/scripts/bulk_icp.py +18 -0
  66. package/scripts/campaign_bump.py +51 -0
  67. package/scripts/capture_thread_media.py +288 -0
  68. package/scripts/check_browser_lock_health.sh +81 -0
  69. package/scripts/check_external_pool_depth.py +253 -0
  70. package/scripts/check_unread_web_chats.py +28 -0
  71. package/scripts/claim_web_chat.py +47 -0
  72. package/scripts/classify_run_error.py +158 -0
  73. package/scripts/claude_job.py +988 -0
  74. package/scripts/clean_stale_singleton.sh +56 -0
  75. package/scripts/cleanup_harness_tabs.py +68 -0
  76. package/scripts/copy_browser_cookies.py +454 -0
  77. package/scripts/counterparty_history.py +350 -0
  78. package/scripts/db.py +57 -0
  79. package/scripts/discover_claude_profiles.py +120 -0
  80. package/scripts/discover_linkedin_candidates.py +984 -0
  81. package/scripts/dm_conversation.py +682 -0
  82. package/scripts/dm_db_update.py +69 -0
  83. package/scripts/dm_engage_helper.py +161 -0
  84. package/scripts/dm_outreach_helper.py +147 -0
  85. package/scripts/dm_outreach_twitter_helper.py +129 -0
  86. package/scripts/dm_send_log.py +106 -0
  87. package/scripts/dm_short_links.py +1084 -0
  88. package/scripts/dump_web_chat_history.py +47 -0
  89. package/scripts/engage_github.py +640 -0
  90. package/scripts/engage_reddit.py +1235 -0
  91. package/scripts/engage_twitter_helper.py +301 -0
  92. package/scripts/engagement_styles.py +1787 -0
  93. package/scripts/enrich_twitter_candidates.py +82 -0
  94. package/scripts/feedback_digest.py +448 -0
  95. package/scripts/fetch_prospect_profile.py +312 -0
  96. package/scripts/fetch_twitter_t1.py +134 -0
  97. package/scripts/find_threads.py +530 -0
  98. package/scripts/follow_gate_log.py +59 -0
  99. package/scripts/funnel_per_day.py +194 -0
  100. package/scripts/generate_daily_human_style.py +494 -0
  101. package/scripts/generation_trace.py +173 -0
  102. package/scripts/get_run_cost.py +107 -0
  103. package/scripts/github_engage_helper.py +93 -0
  104. package/scripts/github_tools.py +509 -0
  105. package/scripts/harness_overlay.py +556 -0
  106. package/scripts/harvest_twitter_following.py +243 -0
  107. package/scripts/heartbeat.sh +70 -0
  108. package/scripts/history_context.py +284 -0
  109. package/scripts/http_api.py +206 -0
  110. package/scripts/human_dm_replies_helper.py +169 -0
  111. package/scripts/identity.py +302 -0
  112. package/scripts/ig_batch_creator.sh +93 -0
  113. package/scripts/ig_post_type_picker.py +243 -0
  114. package/scripts/ig_scrape_transcribe.sh +91 -0
  115. package/scripts/ingest_human_dm_replies.py +271 -0
  116. package/scripts/ingest_web_chat_replies.py +229 -0
  117. package/scripts/install_fleet.py +187 -0
  118. package/scripts/invent_mcp_server.py +350 -0
  119. package/scripts/invent_topics.py +1462 -0
  120. package/scripts/learned_preferences.py +263 -0
  121. package/scripts/li_discovery.py +161 -0
  122. package/scripts/link_edit_helper.py +142 -0
  123. package/scripts/link_tail.py +592 -0
  124. package/scripts/linkedin_api.py +561 -0
  125. package/scripts/linkedin_browser.py +730 -0
  126. package/scripts/linkedin_cooldown.py +128 -0
  127. package/scripts/linkedin_exclusions.py +234 -0
  128. package/scripts/linkedin_killswitch.py +1333 -0
  129. package/scripts/linkedin_search_topic_schema.py +49 -0
  130. package/scripts/linkedin_unipile.py +658 -0
  131. package/scripts/linkedin_url.py +228 -0
  132. package/scripts/log_claude_session.py +636 -0
  133. package/scripts/log_draft.py +143 -0
  134. package/scripts/log_linkedin_search_attempts.py +126 -0
  135. package/scripts/log_post.py +651 -0
  136. package/scripts/log_run.py +364 -0
  137. package/scripts/log_thread_media.py +108 -0
  138. package/scripts/log_twitter_search_attempts.py +150 -0
  139. package/scripts/log_twitter_skips.py +211 -0
  140. package/scripts/lookup_post.py +78 -0
  141. package/scripts/mark_web_chat_processed.py +32 -0
  142. package/scripts/mcp_lock_proxy.py +370 -0
  143. package/scripts/memory_snapshot.py +972 -0
  144. package/scripts/merge_review_queue.py +215 -0
  145. package/scripts/mint_external_pool.py +182 -0
  146. package/scripts/mint_kent_pool.py +249 -0
  147. package/scripts/moltbook_post.py +320 -0
  148. package/scripts/moltbook_tools.py +159 -0
  149. package/scripts/pending_threads.py +188 -0
  150. package/scripts/pick_ig_account.py +177 -0
  151. package/scripts/pick_project.py +208 -0
  152. package/scripts/pick_search_topic.py +771 -0
  153. package/scripts/pick_thread_target.py +279 -0
  154. package/scripts/pick_twitter_thread_target.py +202 -0
  155. package/scripts/podlog_fetch_batch.sh +32 -0
  156. package/scripts/post_github.py +1311 -0
  157. package/scripts/post_reddit.py +2668 -0
  158. package/scripts/precompute_dashboard_stats.py +204 -0
  159. package/scripts/preflight.sh +297 -0
  160. package/scripts/progress.py +88 -0
  161. package/scripts/project_excludes.py +353 -0
  162. package/scripts/project_slugs.py +91 -0
  163. package/scripts/project_stats.py +241 -0
  164. package/scripts/project_stats_json.py +1563 -0
  165. package/scripts/project_topics.py +192 -0
  166. package/scripts/qualified_query_bank.py +436 -0
  167. package/scripts/reap_stale_claude_sessions.py +867 -0
  168. package/scripts/reddit_browser.py +2549 -0
  169. package/scripts/reddit_browser_fetch.py +141 -0
  170. package/scripts/reddit_browser_lock.py +593 -0
  171. package/scripts/reddit_chat_sync.py +710 -0
  172. package/scripts/reddit_query_bank.py +200 -0
  173. package/scripts/reddit_threads_helper.py +151 -0
  174. package/scripts/reddit_tools.py +956 -0
  175. package/scripts/refresh_instagram_tokens.py +280 -0
  176. package/scripts/release-mcpb.sh +497 -0
  177. package/scripts/reply_db.py +334 -0
  178. package/scripts/reply_insert.py +98 -0
  179. package/scripts/reply_risk_digest.py +761 -0
  180. package/scripts/reset-test-machine.sh +602 -0
  181. package/scripts/restore_twitter_session.py +177 -0
  182. package/scripts/ripen_reddit_plan.py +478 -0
  183. package/scripts/run_claude.sh +433 -0
  184. package/scripts/run_moltbook_cycle.py +555 -0
  185. package/scripts/s4l_box_update.sh +226 -0
  186. package/scripts/s4l_channel.py +103 -0
  187. package/scripts/s4l_ctl.sh +75 -0
  188. package/scripts/s4l_env.py +47 -0
  189. package/scripts/saps_activity.py +126 -0
  190. package/scripts/saps_mode.py +328 -0
  191. package/scripts/scan_dm_candidates.py +580 -0
  192. package/scripts/scan_github_replies.py +168 -0
  193. package/scripts/scan_instagram_comments.py +481 -0
  194. package/scripts/scan_moltbook_replies.py +252 -0
  195. package/scripts/scan_pii.py +190 -0
  196. package/scripts/scan_reddit_replies.py +377 -0
  197. package/scripts/scan_twitter_mentions_browser.py +327 -0
  198. package/scripts/scan_twitter_thread_followups.py +299 -0
  199. package/scripts/scan_x_profile.py +384 -0
  200. package/scripts/schedule_state.py +202 -0
  201. package/scripts/scheduled_tasks_snapshot.py +123 -0
  202. package/scripts/score_linkedin_candidates.py +419 -0
  203. package/scripts/score_twitter_candidates.py +718 -0
  204. package/scripts/scrape_linkedin_comment_stats.py +1755 -0
  205. package/scripts/scrape_linkedin_stats_browser.py +52 -0
  206. package/scripts/scrape_reddit_views.py +365 -0
  207. package/scripts/seed_search_queries.py +453 -0
  208. package/scripts/seed_search_topics.py +127 -0
  209. package/scripts/send_web_chat_reply.py +130 -0
  210. package/scripts/sentry_init.py +128 -0
  211. package/scripts/setup_twitter_auth.py +1320 -0
  212. package/scripts/snapshot.py +583 -0
  213. package/scripts/stats.py +2702 -0
  214. package/scripts/stats_helper.py +52 -0
  215. package/scripts/strike_alert.py +783 -0
  216. package/scripts/sweep_post_link_clicks.py +107 -0
  217. package/scripts/sync_ig_to_posts.py +147 -0
  218. package/scripts/test_browser_lock.py +189 -0
  219. package/scripts/test_installation_api.sh +52 -0
  220. package/scripts/test_percard_posting.py +142 -0
  221. package/scripts/top_dud_linkedin_queries.py +71 -0
  222. package/scripts/top_dud_reddit_queries.py +67 -0
  223. package/scripts/top_dud_twitter_queries.py +71 -0
  224. package/scripts/top_dud_twitter_topics.py +102 -0
  225. package/scripts/top_linkedin_queries.py +55 -0
  226. package/scripts/top_omitted_reddit_topics.py +91 -0
  227. package/scripts/top_performers.py +588 -0
  228. package/scripts/top_search_topics.py +180 -0
  229. package/scripts/top_twitter_queries.py +190 -0
  230. package/scripts/twitter_access_check.py +382 -0
  231. package/scripts/twitter_account.py +41 -0
  232. package/scripts/twitter_batch_phase.py +126 -0
  233. package/scripts/twitter_browser.py +2804 -0
  234. package/scripts/twitter_cookie_mirror.py +130 -0
  235. package/scripts/twitter_cycle_helper.py +310 -0
  236. package/scripts/twitter_gen_links.py +287 -0
  237. package/scripts/twitter_post_plan.py +1188 -0
  238. package/scripts/twitter_scan.py +324 -0
  239. package/scripts/twitter_supply_signal.py +57 -0
  240. package/scripts/twitter_threads_helper.py +152 -0
  241. package/scripts/unclaim_web_chat.py +29 -0
  242. package/scripts/update_instagram_stats.py +261 -0
  243. package/scripts/update_linkedin_stats_from_feed.py +328 -0
  244. package/scripts/version.py +72 -0
  245. package/scripts/watchdog_hung_runs.py +343 -0
  246. package/scripts/write_generation_trace.py +73 -0
  247. package/setup/SKILL.md +277 -0
  248. package/skill/amplitude-24h-signups.sh +38 -0
  249. package/skill/archive-old-logs.sh +40 -0
  250. package/skill/audit-dm-staleness.sh +42 -0
  251. package/skill/audit-linkedin.sh +14 -0
  252. package/skill/audit-moltbook.sh +4 -0
  253. package/skill/audit-reddit-resurrect.sh +67 -0
  254. package/skill/audit-reddit.sh +4 -0
  255. package/skill/audit-twitter.sh +4 -0
  256. package/skill/audit.sh +287 -0
  257. package/skill/backfill-twitter-attempts-topic.sh +19 -0
  258. package/skill/backfill-twitter-ghost-posts.sh +24 -0
  259. package/skill/check-external-pool-depth.sh +7 -0
  260. package/skill/check-web-chats.sh +203 -0
  261. package/skill/dm-outreach-linkedin.sh +250 -0
  262. package/skill/dm-outreach-reddit.sh +274 -0
  263. package/skill/dm-outreach-twitter.sh +265 -0
  264. package/skill/engage-dm-replies-linkedin.sh +4 -0
  265. package/skill/engage-dm-replies-reddit.sh +4 -0
  266. package/skill/engage-dm-replies-twitter.sh +4 -0
  267. package/skill/engage-dm-replies.sh +1597 -0
  268. package/skill/engage-linkedin.sh +581 -0
  269. package/skill/engage-moltbook.sh +36 -0
  270. package/skill/engage-reddit.sh +146 -0
  271. package/skill/engage-twitter.sh +467 -0
  272. package/skill/github-engage.sh +176 -0
  273. package/skill/ingest-web-chat-replies.sh +38 -0
  274. package/skill/invent-supply-test.sh +100 -0
  275. package/skill/invent-topics.sh +50 -0
  276. package/skill/lib/linkedin-backend.sh +364 -0
  277. package/skill/lib/platform.sh +48 -0
  278. package/skill/lib/reddit-backend.sh +234 -0
  279. package/skill/lib/twitter-backend.sh +314 -0
  280. package/skill/link-edit-github.sh +136 -0
  281. package/skill/link-edit-moltbook.sh +117 -0
  282. package/skill/link-edit-reddit.sh +201 -0
  283. package/skill/linkedin-presence.sh +182 -0
  284. package/skill/linkedin-recovery.sh +282 -0
  285. package/skill/lock.sh +647 -0
  286. package/skill/memory-snapshot.sh +39 -0
  287. package/skill/precompute-stats.sh +35 -0
  288. package/skill/prewarm-funnel.sh +104 -0
  289. package/skill/refresh-instagram-tokens.sh +57 -0
  290. package/skill/refresh-twitter-following.sh +52 -0
  291. package/skill/reply-risk-digest.sh +31 -0
  292. package/skill/run-cycle-update-guard.sh +44 -0
  293. package/skill/run-draft-and-publish.sh +123 -0
  294. package/skill/run-generate-daily-style.sh +50 -0
  295. package/skill/run-github-launchd.sh +62 -0
  296. package/skill/run-github.sh +102 -0
  297. package/skill/run-instagram-daily.sh +149 -0
  298. package/skill/run-instagram-render.sh +875 -0
  299. package/skill/run-linkedin-launchd.sh +81 -0
  300. package/skill/run-linkedin-unipile.sh +130 -0
  301. package/skill/run-linkedin.sh +1593 -0
  302. package/skill/run-moltbook-launchd.sh +61 -0
  303. package/skill/run-moltbook.sh +38 -0
  304. package/skill/run-overlay-watch.sh +100 -0
  305. package/skill/run-reddit-search-launchd.sh +64 -0
  306. package/skill/run-reddit-search.sh +505 -0
  307. package/skill/run-reddit-threads-double.sh +32 -0
  308. package/skill/run-reddit-threads.sh +847 -0
  309. package/skill/run-scan-moltbook-replies.sh +57 -0
  310. package/skill/run-twitter-cycle-launchd.sh +63 -0
  311. package/skill/run-twitter-cycle-singleton.sh +62 -0
  312. package/skill/run-twitter-cycle.sh +2408 -0
  313. package/skill/run-twitter-threads.sh +592 -0
  314. package/skill/scan-instagram-replies.sh +61 -0
  315. package/skill/scan-twitter-followups.sh +57 -0
  316. package/skill/social-autoposter-update.sh +66 -0
  317. package/skill/stats-instagram.sh +72 -0
  318. package/skill/stats-linkedin.sh +271 -0
  319. package/skill/stats-moltbook.sh +4 -0
  320. package/skill/stats-reddit.sh +4 -0
  321. package/skill/stats-twitter.sh +4 -0
  322. package/skill/stats.sh +521 -0
  323. package/skill/strike-alert.sh +18 -0
  324. package/skill/styles.sh +87 -0
  325. package/skill/sweep-link-clicks.sh +40 -0
  326. package/skill/topics.sh +51 -0
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ # Append one social-autoposter memory/process snapshot.
3
+ #
4
+ # This wrapper is intentionally tiny and does not source .env: command lines can
5
+ # already contain enough context for diagnostics, and the Python sampler redacts
6
+ # likely secrets before writing its JSONL log.
7
+
8
+ set -uo pipefail
9
+
10
+ # SAPS_->S4L_ env mirror (brand rename 2026-07-03): old plists/tasks still
11
+ # export SAPS_*; new code reads S4L_*. Copy names, never values via eval.
12
+ while IFS='=' read -r _k _; do
13
+ case "$_k" in SAPS_*) _n="S4L_${_k#SAPS_}"; eval "[ -n \"\${$_n+x}\" ] || export $_n=\"\${$_k}\"";; esac
14
+ done <<EOF_ENV
15
+ $(env | grep '^SAPS_' | cut -d= -f1 | sed 's/$/=/')
16
+ EOF_ENV
17
+
18
+ REPO_DIR="${REPO_DIR:-$HOME/social-autoposter}"
19
+ LOG_DIR="$REPO_DIR/skill/logs"
20
+ mkdir -p "$LOG_DIR"
21
+
22
+ cd "$REPO_DIR" || exit 2
23
+
24
+ PID_FILE="/tmp/social-autoposter-memory-snapshot.pid"
25
+ if [ -f "$PID_FILE" ]; then
26
+ prev=$(cat "$PID_FILE" 2>/dev/null || true)
27
+ if [ -n "$prev" ] && kill -0 "$prev" 2>/dev/null; then
28
+ echo "[memory-snapshot] previous sampler still active pid=$prev; skipping"
29
+ exit 0
30
+ fi
31
+ fi
32
+ echo "$$" > "$PID_FILE"
33
+ trap 'rm -f "$PID_FILE"' EXIT INT TERM
34
+
35
+ PYTHON_BIN="${S4L_PYTHON:-python3}"
36
+ "$PYTHON_BIN" "$REPO_DIR/scripts/memory_snapshot.py" \
37
+ --output "${S4L_MEMORY_SNAPSHOT_LOG:-$LOG_DIR/memory-snapshots.jsonl}" \
38
+ --top "${S4L_MEMORY_TOP_N:-30}" \
39
+ --max-bytes "${S4L_MEMORY_MAX_BYTES:-104857600}"
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ # precompute-stats.sh — launchd wrapper for scripts/precompute_dashboard_stats.py.
3
+ #
4
+ # Fires every 5 minutes from com.m13v.social-precompute-stats.plist. Writes
5
+ # funnel_stats_<N>d.json, activity_stats_<H>h.json, style_stats_<H>h.json
6
+ # snapshots under skill/cache/ so the dashboard serves instant responses
7
+ # instead of cold-starting HogQL on every request.
8
+ #
9
+ # Keep this wrapper small. All business logic lives in the Python script.
10
+
11
+ set -uo pipefail
12
+
13
+ REPO_DIR="$HOME/social-autoposter"
14
+
15
+ # shellcheck source=/dev/null
16
+ [ -f "$REPO_DIR/.env" ] && source "$REPO_DIR/.env"
17
+
18
+ cd "$REPO_DIR" || exit 2
19
+
20
+ # Single-flight: launchd fires this every 300s, but a single run can take
21
+ # 2-5+ min when PostHog 429s force per-query backoff. Without a lock, slow
22
+ # runs stack into a stampede that saturates HogQL and surfaces as
23
+ # posthog_throttle pills across every project on the dashboard. acquire_lock
24
+ # with a 5s timeout exits 0 cleanly if a prior run is still active.
25
+ # shellcheck source=lock.sh
26
+ source "$REPO_DIR/skill/lock.sh"
27
+ acquire_lock precompute-stats 5
28
+
29
+ RUN_START=$(date +%s)
30
+ python3 "$REPO_DIR/scripts/precompute_dashboard_stats.py"
31
+ EXIT_CODE=$?
32
+ RUN_ELAPSED=$(( $(date +%s) - RUN_START ))
33
+
34
+ echo "[$(date +%H:%M:%S)] === done in ${RUN_ELAPSED}s (exit=${EXIT_CODE}) ==="
35
+ exit "$EXIT_CODE"
@@ -0,0 +1,104 @@
1
+ #!/bin/bash
2
+ # Social Autoposter dashboard funnel cache pre-warmer.
3
+ #
4
+ # /api/funnel/per-day shells out to scripts/funnel_per_day.py which issues one
5
+ # HogQL query per metric per project against PostHog. On a cold cache a single
6
+ # project's call takes 5-25s; without pre-warming the dashboard's per-project
7
+ # breakdown timed out on 19 of 23 projects' funnel fetch on first page-load
8
+ # and rendered them as silent zeros.
9
+ #
10
+ # Strategy: serial calls (not parallel — PostHog rate-limits, and the python
11
+ # script already fans out per-metric internally), longer per-call timeout
12
+ # than the dashboard uses (180s vs 30s frontend), against both the launchd
13
+ # dashboard (3141) and the dev --watch instance (3142, if alive).
14
+ #
15
+ # Scheduled by com.m13v.social-funnel-prewarm.plist every 240s. The server
16
+ # cache TTL is 300s, so a 240s cadence keeps cache continuously hot.
17
+
18
+ set -uo pipefail
19
+
20
+ REPO_DIR="${REPO_DIR:-/Users/matthewdi/social-autoposter}"
21
+ LOG_DIR="${REPO_DIR}/skill/logs"
22
+ LOG_FILE="${LOG_DIR}/prewarm-funnel.log"
23
+ mkdir -p "$LOG_DIR"
24
+
25
+ ts() { date "+%Y-%m-%dT%H:%M:%S%z"; }
26
+ log() { echo "[$(ts)] $*" >> "$LOG_FILE"; }
27
+
28
+ # Single-instance guard. A full cycle takes 2-5min (25 projects x 2 day-windows
29
+ # x N ports, each call 5-180s); launchd fires every 240s, so without this guard
30
+ # the script stacks (saw 2 stale processes from 11:22 + 11:33 on 2026-05-19
31
+ # both still running at 11:35, multiplying PostHog + dashboard pg-pool load and
32
+ # wedging both Get Started cards and per-project breakdown).
33
+ # macOS ships no flock(1), so we use a PID file: a previous process's PID is
34
+ # considered live iff `kill -0 PID` succeeds and the proc is still bash.
35
+ PID_FILE="/tmp/social-autoposter-prewarm-funnel.pid"
36
+ if [ -f "$PID_FILE" ]; then
37
+ prev=$(cat "$PID_FILE" 2>/dev/null || true)
38
+ if [ -n "$prev" ] && kill -0 "$prev" 2>/dev/null && ps -p "$prev" -o comm= 2>/dev/null | grep -qE "bash|sh"; then
39
+ log "another prewarm cycle (pid=$prev) in progress; skipping this tick"
40
+ exit 0
41
+ fi
42
+ fi
43
+ echo "$$" > "$PID_FILE"
44
+ trap 'rm -f "$PID_FILE"' EXIT INT TERM
45
+
46
+ projects=()
47
+ while IFS= read -r line; do
48
+ [ -n "$line" ] && projects+=("$line")
49
+ done < <(jq -r '.projects[].name' "$REPO_DIR/config.json")
50
+
51
+ # Discover live dashboard ports. Probe root URL (no auth, cheap).
52
+ ports=()
53
+ for port in 3141 3142; do
54
+ if curl -sS -o /dev/null -w "%{http_code}" --max-time 3 "http://127.0.0.1:$port/" 2>/dev/null | grep -qE "^(200|301|302|401|403)$"; then
55
+ ports+=("$port")
56
+ fi
57
+ done
58
+
59
+ if [ "${#ports[@]}" -eq 0 ]; then
60
+ log "no dashboard listeners on 3141 or 3142; bailing"
61
+ exit 0
62
+ fi
63
+
64
+ log "start projects=${#projects[@]} ports=${ports[*]}"
65
+
66
+ # Warm one call at a time. The bottleneck is PostHog HogQL latency, not local
67
+ # CPU; serializing means cache builds up monotonically as each project lands.
68
+ # Per-call timeout 180s — generous enough that even worst-case cold projects
69
+ # finish, but capped so a wedged PostHog can't hang the launchd job forever.
70
+ ok=0
71
+ fail=0
72
+ slow=0
73
+
74
+ call_one() {
75
+ local url="$1"
76
+ local label="$2"
77
+ local t code
78
+ read -r code t < <(curl -sS -o /dev/null -w "%{http_code} %{time_total}" --max-time 180 "$url" 2>/dev/null || echo "000 -1")
79
+ if [ "$code" = "200" ]; then
80
+ ok=$((ok+1))
81
+ # Anything over 10s is "slow"; useful signal that the cache was cold here.
82
+ if awk "BEGIN{exit !($t > 10)}"; then slow=$((slow+1)); fi
83
+ else
84
+ fail=$((fail+1))
85
+ log "fail $label code=$code time=${t}s"
86
+ fi
87
+ }
88
+
89
+ for port in "${ports[@]}"; do
90
+ for days in 30 91; do
91
+ # Top-chart "all projects" rollup first — most important call, and it
92
+ # populates the PostHog-side connection cache for the per-project loop
93
+ # that follows.
94
+ call_one "http://127.0.0.1:$port/api/funnel/per-day?days=$days" \
95
+ "port=$port days=$days project=__all__"
96
+ for p in "${projects[@]}"; do
97
+ enc=$(printf '%s' "$p" | jq -sRr @uri)
98
+ call_one "http://127.0.0.1:$port/api/funnel/per-day?days=$days&project=$enc" \
99
+ "port=$port days=$days project=$p"
100
+ done
101
+ done
102
+ done
103
+
104
+ log "done ok=$ok fail=$fail slow=$slow"
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # refresh-instagram-tokens.sh — Refresh Instagram Graph API long-lived tokens
3
+ # before they expire.
4
+ #
5
+ # IG long-lived tokens last ~60 days; this job runs daily and refreshes any
6
+ # token within REFRESH_BUFFER_DAYS (default 14d) of expiry. The .env file at
7
+ # ~/instagram-graph-api/.env is rewritten atomically on success.
8
+ #
9
+ # Lightweight (no lock needed — read+write to a file we own, no browser/MCP)
10
+ # but we take instagram-poster anyway so a poster/stats/scan run that's mid-
11
+ # flight can finish reading the existing token before we swap it.
12
+ #
13
+ # Logs: skill/logs/refresh-instagram-tokens-YYYY-MM-DD_HHMMSS.log
14
+
15
+ set -uo pipefail
16
+
17
+ REPO_DIR="$HOME/social-autoposter"
18
+ LOG_DIR="$REPO_DIR/skill/logs"
19
+ mkdir -p "$LOG_DIR"
20
+ LOG_FILE="$LOG_DIR/refresh-instagram-tokens-$(date +%Y-%m-%d_%H%M%S).log"
21
+
22
+ log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
23
+ log "=== refresh-instagram-tokens fire: $(date) ==="
24
+
25
+ RUN_START=$(date +%s)
26
+
27
+ # shellcheck source=lock.sh
28
+ source "$REPO_DIR/skill/lock.sh"
29
+ acquire_lock instagram-poster 30
30
+
31
+ OUTPUT_FILE="/tmp/refresh-instagram-tokens-$$.out"
32
+ if ! /opt/homebrew/bin/python3.11 "$REPO_DIR/scripts/refresh_instagram_tokens.py" 2>>"$LOG_FILE" | tee -a "$LOG_FILE" >"$OUTPUT_FILE"; then
33
+ log "refresh_instagram_tokens.py exited non-zero"
34
+ REFRESHED=0; SKIPPED=0; FAILED=0; ACCOUNTS=0
35
+ else
36
+ SUMMARY=$(grep '^SUMMARY:' "$OUTPUT_FILE" | tail -1)
37
+ REFRESHED=$(echo "$SUMMARY" | sed -n 's/.*REFRESHED=\([0-9]*\).*/\1/p'); REFRESHED=${REFRESHED:-0}
38
+ SKIPPED=$(echo "$SUMMARY" | sed -n 's/.*SKIPPED=\([0-9]*\).*/\1/p'); SKIPPED=${SKIPPED:-0}
39
+ FAILED=$(echo "$SUMMARY" | sed -n 's/.*FAILED=\([0-9]*\).*/\1/p'); FAILED=${FAILED:-0}
40
+ ACCOUNTS=$(echo "$SUMMARY" | sed -n 's/.*ACCOUNTS=\([0-9]*\).*/\1/p'); ACCOUNTS=${ACCOUNTS:-0}
41
+ fi
42
+ rm -f "$OUTPUT_FILE"
43
+
44
+ RUN_ELAPSED=$(( $(date +%s) - RUN_START ))
45
+
46
+ log "logging run: refreshed=$REFRESHED skipped=$SKIPPED failed=$FAILED accounts=$ACCOUNTS elapsed=${RUN_ELAPSED}s"
47
+
48
+ /opt/homebrew/bin/python3.11 "$REPO_DIR/scripts/log_run.py" \
49
+ --script "refresh_instagram_tokens" \
50
+ --posted "$REFRESHED" \
51
+ --skipped "$SKIPPED" \
52
+ --failed "$FAILED" \
53
+ --cost 0 \
54
+ --elapsed "$RUN_ELAPSED" >>"$LOG_FILE" 2>&1 || log "log_run.py failed"
55
+
56
+ log "=== refresh-instagram-tokens done ==="
57
+ exit 0
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # refresh-twitter-following.sh — refresh the cached "who we follow" list for X.
3
+ #
4
+ # Scrapes x.com/<handle>/following via the harness Chrome and uploads the set to
5
+ # /api/v1/followed-accounts. score_twitter_candidates.py's follow-gate reads that
6
+ # set to skip discovered threads whose author we already follow. The follow list
7
+ # changes slowly, so launchd fires this a few times a day
8
+ # (com.m13v.social-refresh-twitter-following).
9
+ #
10
+ # Uses the SAME shared "twitter-browser" lock + harness bootstrap as
11
+ # engage-twitter.sh / run-twitter-cycle.sh, so it never races a live cycle. On
12
+ # lock contention it skips this run (exit 0) and retries next schedule.
13
+
14
+ set -uo pipefail
15
+
16
+ LOG_DIR="$HOME/social-autoposter/skill/logs"
17
+ mkdir -p "$LOG_DIR"
18
+ LOG_FILE="$LOG_DIR/refresh-twitter-following-$(date +%Y-%m-%d_%H%M%S).log"
19
+ log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
20
+
21
+ REPO_DIR="$HOME/social-autoposter"
22
+
23
+ # Shared twitter-browser lock (lock.sh installs the EXIT-trap release) + harness
24
+ # bootstrap (sets/export TWITTER_CDP_URL, provides ensure_twitter_browser_for_backend).
25
+ # shellcheck source=/dev/null
26
+ source "$(dirname "$0")/lock.sh"
27
+ # shellcheck source=/dev/null
28
+ source "$(dirname "$0")/lib/twitter-backend.sh"
29
+
30
+ log "=== Refresh Twitter following list: $(date) ==="
31
+ log "Acquiring twitter-browser lock (pid=$$)..."
32
+ if ! acquire_lock "twitter-browser" 1800 2>>"$LOG_FILE"; then
33
+ log "twitter-browser busy (a cycle is running); skipping this refresh."
34
+ exit 0
35
+ fi
36
+ log "twitter-browser lock held (pid=$$)"
37
+
38
+ # Probe + launch harness Chrome on port 9555 if needed, then sweep leftover tabs.
39
+ ensure_twitter_browser_for_backend 2>&1 | tee -a "$LOG_FILE"
40
+
41
+ # Load .env so http_api.py picks up AUTOPOSTER_API_BASE / AUTOPOSTER_API_KEY.
42
+ # shellcheck source=/dev/null
43
+ [ -f "$REPO_DIR/.env" ] && source "$REPO_DIR/.env"
44
+
45
+ log "Scraping following list + uploading to /api/v1/followed-accounts..."
46
+ python3 "$REPO_DIR/scripts/harvest_twitter_following.py" 2>&1 | tee -a "$LOG_FILE"
47
+ RC=${PIPESTATUS[0]}
48
+ log "harvest_twitter_following.py exit code: $RC"
49
+
50
+ # Exit 0 regardless: a benign incomplete-scrape (rc=3) or empty (rc=2) should not
51
+ # flag the launchd job as failed; the next schedule retries.
52
+ exit 0
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+ # reply-risk-digest.sh — daily operator email summarizing risky/insightful
3
+ # inbound replies to our replies.
4
+ #
5
+ # Wired by launchd/com.m13v.social-reply-risk-digest.plist. The Python script
6
+ # does the DB read, optional Claude synthesis, and Gmail send.
7
+
8
+ set -uo pipefail
9
+
10
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
11
+ LOG_DIR="$REPO_DIR/skill/logs"
12
+ mkdir -p "$LOG_DIR"
13
+
14
+ LOG_FILE="$LOG_DIR/reply-risk-digest-$(date +%Y-%m-%d_%H%M%S).log"
15
+
16
+ if [ -f "$REPO_DIR/.env" ]; then
17
+ set -a
18
+ # shellcheck disable=SC1091
19
+ source "$REPO_DIR/.env"
20
+ set +a
21
+ fi
22
+
23
+ cd "$REPO_DIR" || exit 1
24
+
25
+ {
26
+ echo "=== $(date -u +%Y-%m-%dT%H:%M:%SZ) reply-risk-digest ==="
27
+ /usr/bin/env python3 scripts/reply_risk_digest.py --hours 24 --platform x
28
+ RC=$?
29
+ echo "=== exit_code=$RC ==="
30
+ exit "$RC"
31
+ } >> "$LOG_FILE" 2>&1
@@ -0,0 +1,44 @@
1
+ #!/bin/bash
2
+ # run-cycle-update-guard.sh — self-update guard that runs IMMEDIATELY BEFORE
3
+ # the twitter cycle, then hands off to the real cycle wrapper.
4
+ #
5
+ # WHY A WRAPPER (and not an edit to run-twitter-cycle-singleton.sh):
6
+ # The singleton is a locked pipeline file (chflags uchg). Per repo policy we
7
+ # never unlock it. So the per-cycle self-update lives here, in front of it:
8
+ # the launchd plist calls THIS script, which (throttled) checks for a newer
9
+ # release, updates if behind, then `exec`s the singleton unchanged.
10
+ #
11
+ # THROTTLE: a headless cycle fires often (every ~60s). We do NOT want a network
12
+ # `npm view` on every fire. The version check runs at most once per
13
+ # CHECK_INTERVAL_SECS (default 6h), gated by a stamp file. In between, this
14
+ # wrapper is a near-instant pass-through.
15
+ #
16
+ # DEV SAFETY: social-autoposter-update.sh refuses to update a .git checkout, so
17
+ # this guard is a no-op update on a dev box (it still execs the cycle).
18
+
19
+ set -u
20
+
21
+ REPO_DIR="${S4L_REPO_DIR:-$HOME/social-autoposter}"
22
+ GUARD_DIR="$REPO_DIR/skill"
23
+ UPDATER="$GUARD_DIR/social-autoposter-update.sh"
24
+ SINGLETON="$GUARD_DIR/run-twitter-cycle-singleton.sh"
25
+ STAMP="$REPO_DIR/skill/logs/.last-update-check"
26
+ CHECK_INTERVAL_SECS="${S4L_UPDATE_CHECK_INTERVAL_SECS:-21600}" # 6h
27
+
28
+ now="$(date +%s)"
29
+ last=0
30
+ [ -f "$STAMP" ] && last="$(cat "$STAMP" 2>/dev/null || echo 0)"
31
+ # normalize non-numeric stamp to 0
32
+ case "$last" in (*[!0-9]*) last=0 ;; esac
33
+
34
+ if [ $(( now - last )) -ge "$CHECK_INTERVAL_SECS" ]; then
35
+ mkdir -p "$(dirname "$STAMP")" 2>/dev/null || true
36
+ echo "$now" > "$STAMP" 2>/dev/null || true
37
+ if [ -x "$UPDATER" ]; then
38
+ # Never let an update hiccup block the posting cycle: run it, ignore failure.
39
+ bash "$UPDATER" || true
40
+ fi
41
+ fi
42
+
43
+ # Hand off to the real (locked) cycle wrapper, preserving any args/env.
44
+ exec /bin/bash "$SINGLETON" "$@"
@@ -0,0 +1,123 @@
1
+ #!/bin/bash
2
+ # run-draft-and-publish.sh — the launchd kicker entrypoint for the queue-backed
3
+ # draft autopilot (2026-06-24). It is the ONLY way cards are produced on a
4
+ # customer box: there is no host-draft scenario.
5
+ #
6
+ # Runs the REAL pipeline in DRAFT_ONLY mode (inheriting the kicker plist's
7
+ # DRAFT_ONLY=1 / S4L_CLAUDE_PROVIDER=queue env, so Phase 2b drafting routes
8
+ # through the job queue and is drafted by the scheduled-task worker), then MERGES
9
+ # the drafts it produced into the review-queue cards the menu bar shows. Without
10
+ # this merge the cycle's plan would sit in a /tmp batch file nobody reads.
11
+ set -uo pipefail
12
+
13
+ # SAPS_->S4L_ env mirror (brand rename 2026-07-03): old plists/tasks still
14
+ # export SAPS_*; new code reads S4L_*. Copy names, never values via eval.
15
+ while IFS='=' read -r _k _; do
16
+ case "$_k" in SAPS_*) _n="S4L_${_k#SAPS_}"; eval "[ -n \"\${$_n+x}\" ] || export $_n=\"\${$_k}\"";; esac
17
+ done <<EOF_ENV
18
+ $(env | grep '^SAPS_' | cut -d= -f1 | sed 's/$/=/')
19
+ EOF_ENV
20
+
21
+ REPO_DIR="${S4L_REPO_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
22
+ PY="${S4L_PYTHON:-python3}"
23
+
24
+ OUT="$(mktemp -t saps_draft_publish.XXXXXX)"
25
+ HB_PID="" # scan-phase heartbeat (started below); torn down by the EXIT trap
26
+ # Clear the menu-bar activity signal on ANY exit so a crash/early-exit mid-cycle
27
+ # never leaves a stuck "scanning/drafting" label, and stop the heartbeat so it
28
+ # can't outlive the cycle. Best-effort; the || true keeps the trap from changing
29
+ # the cycle's exit code.
30
+ trap 'kill "$HB_PID" 2>/dev/null || true; rm -f "$OUT"; "$PY" "$REPO_DIR/scripts/saps_activity.py" clear 2>/dev/null || true' EXIT
31
+
32
+ # Narrate the scan phase, GRANULARLY. The CDP scan runs inside the (locked)
33
+ # run-twitter-cycle.sh which has no activity writer; this covers that window until
34
+ # the queue provider flips the label to "finding threads"/"drafting replies".
35
+ # Instead of a frozen "scanning X for threads" for the whole multi-minute scan,
36
+ # each heartbeat recomputes elapsed and scrapes THIS cycle's own stdout ($OUT, the
37
+ # tee target below) for live progress — queries run, and candidates found once
38
+ # Phase 1 reports them — so the menu bar actually moves. Reads $OUT only; never
39
+ # touches the locked cycle. heartbeat() re-stamps ONLY while the state is still
40
+ # "scanning", so once the provider advances the phase it goes quiet (no flicker).
41
+ "$PY" "$REPO_DIR/scripts/saps_activity.py" write scanning "scan: starting" 2>/dev/null || true
42
+ SCAN_T0=$(date +%s)
43
+ (
44
+ while true; do
45
+ sleep 20
46
+ _el=$(( $(date +%s) - SCAN_T0 ))
47
+ if [ "$_el" -lt 60 ]; then _dur="${_el}s"; else _dur="$(( _el / 60 ))m"; fi
48
+ _q=$(grep -c "kept=" "$OUT" 2>/dev/null || true); _q=${_q:-0}
49
+ # Total planned queries IS announced upfront by Phase 1:
50
+ # "Lean Phase 1: executing 118 queries via browser-harness CDP"
51
+ # so show K/total once that line lands (it precedes the per-query "kept=" lines).
52
+ _total=$(grep -oE "executing [0-9]+ queries" "$OUT" 2>/dev/null | tail -1 | grep -oE "[0-9]+" | head -1 || true)
53
+ if [ -n "$_total" ]; then _qpart="${_q}/${_total}"; else _qpart="${_q}"; fi
54
+ _found=$(grep -oE "Batch has [0-9]+" "$OUT" 2>/dev/null | tail -1 | grep -oE "[0-9]+" | tail -1 || true)
55
+ if [ -n "$_found" ]; then
56
+ _lbl="scan: ${_dur} · ${_qpart}, ${_found} found"
57
+ else
58
+ _lbl="scan: ${_dur} · ${_qpart}"
59
+ fi
60
+ "$PY" "$REPO_DIR/scripts/saps_activity.py" heartbeat scanning "$_lbl" 2>/dev/null || true
61
+ done
62
+ ) &
63
+ HB_PID=$!
64
+
65
+ # Engagement mode (2026-06-26). The menu-bar toggle writes mode.json; this reads
66
+ # it and, in personal_brand mode, exports S4L_FORCE_PROJECT=<persona project> and
67
+ # TWITTER_TAIL_LINK_RATE=0 so the (locked) cycle below drafts link-free organic
68
+ # replies for the persona instead of the normal weighted product pick. In the
69
+ # default promotion mode it exports nothing and the cycle runs exactly as before.
70
+ # Read at cycle runtime (NOT baked into the plist) so flipping the toggle takes
71
+ # effect on the very next cycle with no launchd reload. Best-effort: any failure
72
+ # leaves the env untouched and the promotion pipeline runs.
73
+ eval "$("$PY" "$REPO_DIR/scripts/saps_mode.py" env 2>/dev/null || true)"
74
+ if [ -n "${S4L_FORCE_PROJECT:-}" ]; then
75
+ echo "[run-draft-and-publish] personal_brand mode: forcing project '$S4L_FORCE_PROJECT' (link-free)" >&2
76
+ fi
77
+
78
+ # First-run onboarding boost (2026-07-02). The MCP server drops
79
+ # first-run-boost.json into the state dir when it installs the kicker for the
80
+ # very first time. While the marker is live, widen the draft discovery window
81
+ # to 48h (vs the standard 24h draft window) and lift the top-1 card cap so the
82
+ # user's FIRST review batch surfaces several REAL drafts instead of one (or
83
+ # none). The marker is deleted the moment a merge actually delivers cards, or
84
+ # after 24h without any, so every later cycle runs the standard logic.
85
+ BOOST_MARKER="${S4L_STATE_DIR:-$HOME/.social-autoposter-mcp}/first-run-boost.json"
86
+ BOOST_ACTIVE=0
87
+ if [ -f "$BOOST_MARKER" ]; then
88
+ if [ -n "$(find "$BOOST_MARKER" -mmin +1440 2>/dev/null)" ]; then
89
+ rm -f "$BOOST_MARKER"
90
+ echo "[run-draft-and-publish] first-run boost expired (>24h, no cards produced); removed" >&2
91
+ else
92
+ BOOST_ACTIVE=1
93
+ export S4L_DRAFT_FRESHNESS_HOURS="${S4L_FIRST_RUN_FRESHNESS_HOURS:-48}"
94
+ export S4L_TWITTER_POST_TOP_N="${S4L_FIRST_RUN_TOP_N:-5}"
95
+ echo "[run-draft-and-publish] first-run boost active: freshness=${S4L_DRAFT_FRESHNESS_HOURS}h top_n=${S4L_TWITTER_POST_TOP_N}" >&2
96
+ fi
97
+ fi
98
+
99
+ # Run the cycle; tee stdout so we can scan it for the DRAFT_ONLY_PLAN marker.
100
+ # Phase 2b blocks on the queue until the worker drafts it, so this can take a
101
+ # few minutes — that is expected.
102
+ bash "$REPO_DIR/skill/run-twitter-cycle.sh" 2>&1 | tee "$OUT"
103
+ RC=${PIPESTATUS[0]}
104
+
105
+ # Deliver the cycle's drafts into the cards.
106
+ MARKER="$(grep -oE 'DRAFT_ONLY_PLAN=\S+\.json' "$OUT" | tail -1)"
107
+ if [ -n "$MARKER" ]; then
108
+ # merge_review_queue prints ONLY to stderr; capture and re-emit verbatim on
109
+ # stderr (those [merge_review_queue] marker lines are load-bearing) so the
110
+ # first-run boost can read the merged count.
111
+ MERGE_OUT="$("$PY" "$REPO_DIR/scripts/merge_review_queue.py" --plan-from-marker "$MARKER" 2>&1 || true)"
112
+ [ -n "$MERGE_OUT" ] && printf '%s\n' "$MERGE_OUT" >&2
113
+ # Consume the first-run boost the moment a merge actually delivers cards, so
114
+ # the widened window applies to exactly one successful first batch.
115
+ if [ "$BOOST_ACTIVE" = "1" ] && printf '%s' "$MERGE_OUT" | grep -qE 'merged [1-9][0-9]* new draft'; then
116
+ rm -f "$BOOST_MARKER"
117
+ echo "[run-draft-and-publish] first-run boost consumed (cards delivered)" >&2
118
+ fi
119
+ else
120
+ echo "[run-draft-and-publish] no DRAFT_ONLY_PLAN marker (cycle rc=$RC); nothing to merge" >&2
121
+ fi
122
+
123
+ exit "$RC"
@@ -0,0 +1,50 @@
1
+ #!/bin/bash
2
+ # run-generate-daily-style.sh — Synthesize ONE new human-derived
3
+ # engagement style per platform per fire, from the last 24h of top
4
+ # human replies on each platform.
5
+ #
6
+ # Cadence (launchd, com.m13v.social-daily-human-style.plist): once per day
7
+ # at 08:00 local time.
8
+ #
9
+ # Wraps scripts/generate_daily_human_style.py — which queries
10
+ # thread_top_replies per platform, calls Claude via run_claude.sh, and
11
+ # POSTs each synthesized style to the s4l.ai API route
12
+ # /api/v1/engagement-styles/registry with kind='human_derived' and
13
+ # platform=<platform>. Rows land in engagement_styles_registry alongside
14
+ # seeds and model-invented styles. The engagement_styles picker reads
15
+ # the latest active row per platform with HUMAN_DERIVED_RATE_BY_PLATFORM
16
+ # probability on each pick.
17
+ #
18
+ # Exit codes:
19
+ # 0 — style inserted, OR insufficient input (< 3 replies, logged + skipped)
20
+ # 1 — real failure (DB error, Claude error, JSON parse failure)
21
+ #
22
+ # Logs: skill/logs/daily-human-style-YYYY-MM-DD_HHMMSS.log
23
+
24
+ set -uo pipefail
25
+
26
+ REPO_DIR="$HOME/social-autoposter"
27
+ LOG_DIR="$REPO_DIR/skill/logs"
28
+ mkdir -p "$LOG_DIR"
29
+
30
+ LOG_FILE="$LOG_DIR/daily-human-style-$(date +%Y-%m-%d_%H%M%S).log"
31
+
32
+ if [ -f "$REPO_DIR/.env" ]; then
33
+ set -a
34
+ # shellcheck disable=SC1091
35
+ source "$REPO_DIR/.env"
36
+ set +a
37
+ fi
38
+
39
+ log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
40
+
41
+ log "starting daily human-style synthesizer"
42
+
43
+ # The Python script invokes scripts/run_claude.sh internally for the Claude
44
+ # call (so cost lands in claude_sessions under script_tag=daily-human-style).
45
+ # We just stream its stdout/stderr to the log file here.
46
+ /usr/bin/python3 "$REPO_DIR/scripts/generate_daily_human_style.py" 2>&1 | tee -a "$LOG_FILE"
47
+ RC=${PIPESTATUS[0]}
48
+
49
+ log "synthesizer exit code: $RC"
50
+ exit "$RC"
@@ -0,0 +1,62 @@
1
+ #!/bin/bash
2
+ # run-github-launchd.sh — detach wrapper invoked by launchd.
3
+ #
4
+ # Why this exists:
5
+ # launchd's StartInterval silently SUPPRESSES a scheduled fire when the prior
6
+ # invocation of the same Label is still alive. The github cycle does a T0
7
+ # issue search, sleeps ~600s for momentum, then re-fetches and posts. Total
8
+ # runtime regularly exceeds 15 min, so without this wrapper roughly half of
9
+ # the scheduled fires got dropped.
10
+ #
11
+ # How it works:
12
+ # Python double-fork daemon idiom — first fork gives launchd a parent that
13
+ # exits immediately (so the job is marked complete in milliseconds), setsid
14
+ # detaches the session, second fork prevents reacquiring a controlling
15
+ # terminal, then we exec the real pipeline. macOS lacks `setsid(1)` and
16
+ # `nohup ... & disown` is not enough because launchd reaps the wrapper's
17
+ # pgid, taking the nohup child with it.
18
+ #
19
+ # Cross-cycle safety:
20
+ # post_github.py applies an already_posted filter against the posts table
21
+ # before drafting, so overlapping cycles will not double-post the same
22
+ # issue. gh CLI is API-only (no shared browser/profile), so there is no
23
+ # browser-level lock to coordinate.
24
+
25
+ REPO_DIR="$HOME/social-autoposter"
26
+ LOG_DIR="$REPO_DIR/skill/logs"
27
+ mkdir -p "$LOG_DIR"
28
+
29
+ SCRIPT="$REPO_DIR/skill/run-github.sh"
30
+ OUT="$LOG_DIR/launchd-github-stdout.log"
31
+ ERR="$LOG_DIR/launchd-github-stderr.log"
32
+
33
+ # Preflight (added 2026-05-02): skip cleanly if Claude is blocked on a
34
+ # quota cap, or if the system is under memory pressure. See
35
+ # scripts/preflight.sh for full design.
36
+ SA_PREFLIGHT_SCRIPT="run-github"
37
+ source "$REPO_DIR/scripts/preflight.sh"
38
+ preflight_skip_if_claude_blocked
39
+ preflight_skip_if_jetsam_pressure
40
+
41
+ exec /usr/bin/python3 -c "
42
+ import os, sys
43
+ script = '$SCRIPT'
44
+ out_log = '$OUT'
45
+ err_log = '$ERR'
46
+
47
+ if os.fork() != 0:
48
+ os._exit(0)
49
+ os.setsid()
50
+
51
+ if os.fork() != 0:
52
+ os._exit(0)
53
+
54
+ os.chdir('/')
55
+ out_fd = os.open(out_log, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
56
+ err_fd = os.open(err_log, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
57
+ nul_fd = os.open('/dev/null', os.O_RDONLY)
58
+ os.dup2(nul_fd, 0)
59
+ os.dup2(out_fd, 1)
60
+ os.dup2(err_fd, 2)
61
+ os.execv(script, [script])
62
+ "