@m13v/s4l 1.6.197-rc.10
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 +1336 -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 +513 -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
|
@@ -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
|
+
"
|