@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/skill/audit.sh
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# audit.sh — Post audit pipeline.
|
|
3
|
+
#
|
|
4
|
+
# Per-platform mode (preferred, driven by launchd via per-platform wrappers):
|
|
5
|
+
# --platform reddit Reddit API audit via stats.py --reddit-only
|
|
6
|
+
# --platform moltbook Moltbook API audit via stats.py --moltbook-only
|
|
7
|
+
# --platform twitter Twitter API audit via stats.py --twitter-audit
|
|
8
|
+
# --platform linkedin Retired 2026-04-17 (flagged CDP pattern). Engagement
|
|
9
|
+
# stats now collected via stats.sh Step 4 (linkedin-agent
|
|
10
|
+
# MCP, headed Chrome). Branch kept as no-op so the
|
|
11
|
+
# audit-linkedin launchd job doesn't error.
|
|
12
|
+
#
|
|
13
|
+
# Every run also executes the orphan/summary step at the end (DB-only, cheap).
|
|
14
|
+
# With no --platform, runs all four sequentially (legacy manual path).
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
set -uo pipefail
|
|
18
|
+
|
|
19
|
+
# Parse args.
|
|
20
|
+
PLATFORM=""
|
|
21
|
+
while [ $# -gt 0 ]; do
|
|
22
|
+
case "$1" in
|
|
23
|
+
--platform) PLATFORM="${2:-}"; shift 2 ;;
|
|
24
|
+
--platform=*) PLATFORM="${1#--platform=}"; shift ;;
|
|
25
|
+
*) shift ;;
|
|
26
|
+
esac
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
case "$PLATFORM" in
|
|
30
|
+
""|reddit|twitter|linkedin|moltbook) ;;
|
|
31
|
+
*)
|
|
32
|
+
echo "audit.sh: invalid --platform '$PLATFORM' (expected reddit, twitter, linkedin, or moltbook)" >&2
|
|
33
|
+
exit 2
|
|
34
|
+
;;
|
|
35
|
+
esac
|
|
36
|
+
|
|
37
|
+
# Per-platform lock name so all four can run concurrently, but a second
|
|
38
|
+
# invocation of the same platform waits. Legacy no-platform run keeps the
|
|
39
|
+
# original "audit" lock name.
|
|
40
|
+
LOCK_NAME="audit${PLATFORM:+-$PLATFORM}"
|
|
41
|
+
|
|
42
|
+
# Browser-profile lock first (shared across pipelines using the same browser),
|
|
43
|
+
# then the pipeline-specific lock. moltbook has no shared browser profile.
|
|
44
|
+
#
|
|
45
|
+
# Reddit uses the unified Python lease (2026-05-10) — TTL-aware, auto-decays
|
|
46
|
+
# during Claude idle gaps so peer pipelines can use the profile. The MCP
|
|
47
|
+
# proxy heartbeats expires_at on every reddit-agent call. LinkedIn/Twitter
|
|
48
|
+
# still use the bash lock (no MCP-proxy heartbeat wiring yet).
|
|
49
|
+
source "$(dirname "$0")/lock.sh"
|
|
50
|
+
REPO_DIR_FOR_LOCK="$HOME/social-autoposter"
|
|
51
|
+
_release_reddit_lease() {
|
|
52
|
+
timeout 3 python3 "$REPO_DIR_FOR_LOCK/scripts/reddit_browser_lock.py" release 2>/dev/null || true
|
|
53
|
+
}
|
|
54
|
+
case "${PLATFORM:-all}" in
|
|
55
|
+
linkedin) acquire_lock "linkedin-browser" 3600 ;;
|
|
56
|
+
reddit)
|
|
57
|
+
python3 "$REPO_DIR_FOR_LOCK/scripts/reddit_browser_lock.py" acquire --timeout 3600 --ttl 90 2>&1 || \
|
|
58
|
+
echo "WARNING: reddit_browser_lock.py acquire failed; proceeding without lease."
|
|
59
|
+
trap '_release_reddit_lease; _sa_release_locks' EXIT INT TERM HUP
|
|
60
|
+
;;
|
|
61
|
+
twitter|x) acquire_lock "twitter-browser" 3600 ;;
|
|
62
|
+
moltbook) ;;
|
|
63
|
+
all)
|
|
64
|
+
acquire_lock "linkedin-browser" 3600
|
|
65
|
+
python3 "$REPO_DIR_FOR_LOCK/scripts/reddit_browser_lock.py" acquire --timeout 3600 --ttl 90 2>&1 || \
|
|
66
|
+
echo "WARNING: reddit_browser_lock.py acquire failed; proceeding without lease."
|
|
67
|
+
trap '_release_reddit_lease; _sa_release_locks' EXIT INT TERM HUP
|
|
68
|
+
acquire_lock "twitter-browser" 3600
|
|
69
|
+
;;
|
|
70
|
+
esac
|
|
71
|
+
acquire_lock "$LOCK_NAME" 3600
|
|
72
|
+
|
|
73
|
+
# Load secrets
|
|
74
|
+
# shellcheck source=/dev/null
|
|
75
|
+
[ -f "$HOME/social-autoposter/.env" ] && source "$HOME/social-autoposter/.env"
|
|
76
|
+
|
|
77
|
+
REPO_DIR="$HOME/social-autoposter"
|
|
78
|
+
SKILL_FILE="$REPO_DIR/SKILL.md"
|
|
79
|
+
LOG_DIR="$REPO_DIR/skill/logs"
|
|
80
|
+
# HTTP-only lane (2026-06-01): all reads go through the s4l.ai API via
|
|
81
|
+
# scripts/audit_helper.py. No DATABASE_URL, no psql, no fallback.
|
|
82
|
+
AUDIT_HELPER="$REPO_DIR/scripts/audit_helper.py"
|
|
83
|
+
|
|
84
|
+
mkdir -p "$LOG_DIR"
|
|
85
|
+
LOG_TAG="${PLATFORM:-all}"
|
|
86
|
+
LOG_FILE="$LOG_DIR/audit-${LOG_TAG}-$(date +%Y-%m-%d_%H%M%S).log"
|
|
87
|
+
|
|
88
|
+
log() { echo "[$(date +%H:%M:%S)] $*" >> "$LOG_FILE"; echo "[$(date +%H:%M:%S)] $*"; }
|
|
89
|
+
|
|
90
|
+
RUN_START=$(date +%s)
|
|
91
|
+
log "=== Audit Pipeline Run (${LOG_TAG}): $(date) ==="
|
|
92
|
+
|
|
93
|
+
# Decide which steps run for this invocation.
|
|
94
|
+
if [ -z "$PLATFORM" ]; then
|
|
95
|
+
RUN_REDDIT=1; RUN_MOLTBOOK=1; RUN_TWITTER=1; RUN_LINKEDIN=1
|
|
96
|
+
else
|
|
97
|
+
RUN_REDDIT=0; RUN_MOLTBOOK=0; RUN_TWITTER=0; RUN_LINKEDIN=0
|
|
98
|
+
case "$PLATFORM" in
|
|
99
|
+
reddit) RUN_REDDIT=1 ;;
|
|
100
|
+
moltbook) RUN_MOLTBOOK=1 ;;
|
|
101
|
+
twitter) RUN_TWITTER=1 ;;
|
|
102
|
+
linkedin) RUN_LINKEDIN=1 ;;
|
|
103
|
+
esac
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
STEP1_EXIT=0
|
|
107
|
+
STEP2_EXIT=0
|
|
108
|
+
STEP3_EXIT=0
|
|
109
|
+
|
|
110
|
+
# ═══════════════════════════════════════════════════════
|
|
111
|
+
# Reddit API audit
|
|
112
|
+
# ═══════════════════════════════════════════════════════
|
|
113
|
+
if [ "$RUN_REDDIT" -eq 1 ]; then
|
|
114
|
+
log "Reddit: API audit (stats.py --reddit-only)"
|
|
115
|
+
if [ -z "$PLATFORM" ]; then
|
|
116
|
+
# Legacy all-platform path uses the combined default pass which also
|
|
117
|
+
# covers Moltbook + Twitter, so we don't duplicate them below.
|
|
118
|
+
python3 "$REPO_DIR/scripts/stats.py" >> "$LOG_FILE" 2>&1
|
|
119
|
+
else
|
|
120
|
+
python3 "$REPO_DIR/scripts/stats.py" --reddit-only >> "$LOG_FILE" 2>&1
|
|
121
|
+
fi
|
|
122
|
+
STEP1_EXIT=$?
|
|
123
|
+
if [ "$STEP1_EXIT" -ne 0 ]; then
|
|
124
|
+
log "Reddit: FAILED (exit $STEP1_EXIT)"
|
|
125
|
+
else
|
|
126
|
+
log "Reddit: Done"
|
|
127
|
+
fi
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# ═══════════════════════════════════════════════════════
|
|
131
|
+
# Moltbook API audit
|
|
132
|
+
# ═══════════════════════════════════════════════════════
|
|
133
|
+
# Skip in legacy mode — already covered by the combined pass above.
|
|
134
|
+
if [ "$RUN_MOLTBOOK" -eq 1 ] && [ -n "$PLATFORM" ]; then
|
|
135
|
+
log "Moltbook: API audit (stats.py --moltbook-only)"
|
|
136
|
+
python3 "$REPO_DIR/scripts/stats.py" --moltbook-only >> "$LOG_FILE" 2>&1
|
|
137
|
+
MOLTBOOK_EXIT=$?
|
|
138
|
+
if [ "$MOLTBOOK_EXIT" -ne 0 ]; then
|
|
139
|
+
log "Moltbook: FAILED (exit $MOLTBOOK_EXIT)"
|
|
140
|
+
else
|
|
141
|
+
log "Moltbook: Done"
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# ═══════════════════════════════════════════════════════
|
|
146
|
+
# Twitter API audit (fxtwitter — no browser)
|
|
147
|
+
# ═══════════════════════════════════════════════════════
|
|
148
|
+
if [ "$RUN_TWITTER" -eq 1 ]; then
|
|
149
|
+
TWITTER_COUNT=$(python3 "$AUDIT_HELPER" twitter-active-count 2>/dev/null || echo "0")
|
|
150
|
+
|
|
151
|
+
if [ "$TWITTER_COUNT" -gt 0 ]; then
|
|
152
|
+
log "Twitter: API audit — $TWITTER_COUNT active tweets"
|
|
153
|
+
python3 "$REPO_DIR/scripts/stats.py" --twitter-audit >> "$LOG_FILE" 2>&1
|
|
154
|
+
STEP2_EXIT=$?
|
|
155
|
+
if [ "$STEP2_EXIT" -ne 0 ]; then
|
|
156
|
+
log "Twitter: FAILED (exit $STEP2_EXIT)"
|
|
157
|
+
else
|
|
158
|
+
log "Twitter: Done"
|
|
159
|
+
fi
|
|
160
|
+
else
|
|
161
|
+
log "Twitter: SKIPPED — no active Twitter posts to audit"
|
|
162
|
+
fi
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
# ═══════════════════════════════════════════════════════
|
|
166
|
+
# LinkedIn audit — retired 2026-04-17 (flagged CDP fingerprint).
|
|
167
|
+
# Post-engagement stats are now collected in stats.sh Step 4 via the
|
|
168
|
+
# linkedin-agent MCP (headed Chrome). Deletion detection is not currently
|
|
169
|
+
# covered; if needed, extend stats.sh Step 4 to parse 404 / "This post
|
|
170
|
+
# isn't available" screens.
|
|
171
|
+
# ═══════════════════════════════════════════════════════
|
|
172
|
+
if [ "$RUN_LINKEDIN" -eq 1 ]; then
|
|
173
|
+
log "LinkedIn: SKIPPED — CDP audit retired (see stats.sh Step 4 for engagement stats via MCP)"
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# ═══════════════════════════════════════════════════════
|
|
177
|
+
# Orphan / stale post detection + summary (DB-only, every run)
|
|
178
|
+
# ═══════════════════════════════════════════════════════
|
|
179
|
+
log "Orphan/stale detection"
|
|
180
|
+
|
|
181
|
+
ORPHAN_REPORT=$(python3 "$AUDIT_HELPER" orphan-report 2>/dev/null || echo "")
|
|
182
|
+
|
|
183
|
+
BROKEN_URL_COUNT=$(python3 "$AUDIT_HELPER" broken-url-count 2>/dev/null || echo "0")
|
|
184
|
+
|
|
185
|
+
if [ -n "$ORPHAN_REPORT" ]; then
|
|
186
|
+
log "WARNING: Posts with non-standard status:"
|
|
187
|
+
echo "$ORPHAN_REPORT" | while IFS='|' read -r plat stat cnt; do
|
|
188
|
+
log " $plat $stat: $cnt"
|
|
189
|
+
done
|
|
190
|
+
fi
|
|
191
|
+
if [ "$BROKEN_URL_COUNT" -gt 0 ]; then
|
|
192
|
+
log "WARNING: $BROKEN_URL_COUNT active posts with missing/invalid our_url"
|
|
193
|
+
fi
|
|
194
|
+
if [ -z "$ORPHAN_REPORT" ] && [ "$BROKEN_URL_COUNT" = "0" ]; then
|
|
195
|
+
log "Orphan/stale: Clean (no orphans, no broken URLs)"
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
log "Summary"
|
|
199
|
+
|
|
200
|
+
ACTIVE=$(python3 "$AUDIT_HELPER" status-count --status active 2>/dev/null || echo "?")
|
|
201
|
+
DELETED=$(python3 "$AUDIT_HELPER" status-count --status deleted 2>/dev/null || echo "?")
|
|
202
|
+
REMOVED=$(python3 "$AUDIT_HELPER" status-count --status removed 2>/dev/null || echo "?")
|
|
203
|
+
|
|
204
|
+
log "Post status: active=$ACTIVE deleted=$DELETED removed=$REMOVED"
|
|
205
|
+
|
|
206
|
+
# Log run to persistent monitor.
|
|
207
|
+
RUN_ELAPSED=$(( $(date +%s) - RUN_START ))
|
|
208
|
+
AUDIT_FAILED=$(( (STEP1_EXIT != 0 ? 1 : 0) + (STEP2_EXIT != 0 ? 1 : 0) + (STEP3_EXIT != 0 ? 1 : 0) ))
|
|
209
|
+
SCRIPT_TAG="audit${PLATFORM:+-$PLATFORM}"
|
|
210
|
+
|
|
211
|
+
# Sum per-platform STATS_JSON lines emitted by stats.py into log_run.py flags so
|
|
212
|
+
# the dashboard Job History row shows real counters (scanned/checked/changed/
|
|
213
|
+
# replies-refreshed/removed) instead of the legacy posted=<active_count> mush.
|
|
214
|
+
# Each platform's stats.py print is followed by one `STATS_JSON: {...}` line;
|
|
215
|
+
# we read them all from $LOG_FILE and aggregate by kind. Missing keys default to
|
|
216
|
+
# 0 so the existing log_run.py flag surface stays unchanged.
|
|
217
|
+
read -r SCANNED CHECKED CHANGED DELETED ERRORS REPLIES_REFRESHED REPLIES_FRESH THREADS_SCANNED THREADS_WRITTEN <<<"$(
|
|
218
|
+
python3 - "$LOG_FILE" <<'PY'
|
|
219
|
+
import json, sys, re
|
|
220
|
+
log_path = sys.argv[1]
|
|
221
|
+
agg = dict(scanned=0, checked=0, changed=0, deleted=0, errors=0,
|
|
222
|
+
replies_refreshed=0, replies_fresh=0,
|
|
223
|
+
threads_scanned=0, threads_written=0)
|
|
224
|
+
try:
|
|
225
|
+
with open(log_path) as f:
|
|
226
|
+
for line in f:
|
|
227
|
+
m = re.search(r"STATS_JSON:\s*(\{.*\})\s*$", line)
|
|
228
|
+
if not m:
|
|
229
|
+
continue
|
|
230
|
+
try:
|
|
231
|
+
d = json.loads(m.group(1))
|
|
232
|
+
except Exception:
|
|
233
|
+
continue
|
|
234
|
+
kind = d.get("kind")
|
|
235
|
+
if kind == "posts":
|
|
236
|
+
agg["scanned"] += int(d.get("total", 0) or 0)
|
|
237
|
+
agg["checked"] += int(d.get("checked", 0) or 0)
|
|
238
|
+
agg["changed"] += int(d.get("changed", 0) or 0)
|
|
239
|
+
agg["deleted"] += int(d.get("deleted", 0) or 0) + int(d.get("removed", 0) or 0)
|
|
240
|
+
agg["errors"] += int(d.get("errors", 0) or 0)
|
|
241
|
+
elif kind == "replies":
|
|
242
|
+
agg["replies_refreshed"] += int(d.get("updated", 0) or 0)
|
|
243
|
+
agg["replies_fresh"] += int(d.get("fresh", 0) or 0)
|
|
244
|
+
elif kind == "thread_snapshots":
|
|
245
|
+
agg["threads_scanned"] += int(d.get("scanned", 0) or 0)
|
|
246
|
+
agg["threads_written"] += int(d.get("written", 0) or 0)
|
|
247
|
+
except FileNotFoundError:
|
|
248
|
+
pass
|
|
249
|
+
print(agg["scanned"], agg["checked"], agg["changed"], agg["deleted"],
|
|
250
|
+
agg["errors"], agg["replies_refreshed"], agg["replies_fresh"],
|
|
251
|
+
agg["threads_scanned"], agg["threads_written"])
|
|
252
|
+
PY
|
|
253
|
+
)"
|
|
254
|
+
SCANNED="${SCANNED:-0}"
|
|
255
|
+
CHECKED="${CHECKED:-0}"
|
|
256
|
+
CHANGED="${CHANGED:-0}"
|
|
257
|
+
DELETED="${DELETED:-0}"
|
|
258
|
+
ERRORS="${ERRORS:-0}"
|
|
259
|
+
REPLIES_REFRESHED="${REPLIES_REFRESHED:-0}"
|
|
260
|
+
REPLIES_FRESH="${REPLIES_FRESH:-0}"
|
|
261
|
+
THREADS_SCANNED="${THREADS_SCANNED:-0}"
|
|
262
|
+
THREADS_WRITTEN="${THREADS_WRITTEN:-0}"
|
|
263
|
+
|
|
264
|
+
# Roll API errors from stats.py into the dashboard `failed` pill alongside
|
|
265
|
+
# step-exit counts (same convention stats.sh uses).
|
|
266
|
+
AUDIT_FAILED=$(( AUDIT_FAILED + ERRORS ))
|
|
267
|
+
|
|
268
|
+
log "Per-run counters: scanned=$SCANNED checked=$CHECKED changed=$CHANGED removed=$DELETED errors=$ERRORS replies_refreshed=$REPLIES_REFRESHED replies_fresh=$REPLIES_FRESH thread_snapshots_written=$THREADS_WRITTEN"
|
|
269
|
+
|
|
270
|
+
python3 "$REPO_DIR/scripts/log_run.py" \
|
|
271
|
+
--script "$SCRIPT_TAG" \
|
|
272
|
+
--posted 0 \
|
|
273
|
+
--skipped 0 \
|
|
274
|
+
--failed "$AUDIT_FAILED" \
|
|
275
|
+
--replies-refreshed "$REPLIES_REFRESHED" \
|
|
276
|
+
--checked "$CHECKED" \
|
|
277
|
+
--updated "$CHANGED" \
|
|
278
|
+
--removed "$DELETED" \
|
|
279
|
+
--scanned "$SCANNED" \
|
|
280
|
+
--changed "$CHANGED" \
|
|
281
|
+
--cost 0 \
|
|
282
|
+
--elapsed "$RUN_ELAPSED"
|
|
283
|
+
|
|
284
|
+
log "=== Audit Pipeline complete (${LOG_TAG}): $(date) ==="
|
|
285
|
+
|
|
286
|
+
# Clean up old logs (keep last 14 days) — covers both audit-all-* and audit-<platform>-*.
|
|
287
|
+
find "$LOG_DIR" -name "audit-*.log" -mtime +14 -delete 2>/dev/null || true
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Periodic backfill of twitter_search_attempts.search_topic from
|
|
3
|
+
# twitter_candidates (Pass A) + (batch_id, project_name) fanout (Pass B).
|
|
4
|
+
# Runs every 5 minutes via com.m13v.social-twitter-attempt-topic-backfill.
|
|
5
|
+
# Idempotent: each pass only touches rows where search_topic IS NULL, so
|
|
6
|
+
# repeated runs converge to zero work once the topics are populated.
|
|
7
|
+
#
|
|
8
|
+
# Why this exists: skill/run-twitter-cycle.sh and score_twitter_candidates.py
|
|
9
|
+
# are both chflags-locked. The picker (pick_search_topic.py) stamps
|
|
10
|
+
# twitter_candidates.search_topic when it scores tweets, but the attempt row
|
|
11
|
+
# stays NULL because the SCAN_SCHEMA in the locked shell doesn't carry the
|
|
12
|
+
# topic through queries_used. This script closes that gap.
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
REPO_DIR="${REPO_DIR:-/Users/matthewdi/social-autoposter}"
|
|
16
|
+
PYTHON_BIN="${PYTHON_BIN:-/opt/homebrew/bin/python3.11}"
|
|
17
|
+
|
|
18
|
+
cd "$REPO_DIR"
|
|
19
|
+
exec "$PYTHON_BIN" scripts/backfill_twitter_attempts_topic.py --days 14
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Periodic recovery of "ghost" Twitter posts: replies that landed on x.com but
|
|
3
|
+
# whose POST /api/v1/posts log call failed (rate-limit cap, transient 500, or
|
|
4
|
+
# timeout), so twitter_post_plan.py marked the candidate 'skipped' and reported
|
|
5
|
+
# log_post_no_id. The tweet is live; the DB forgot it.
|
|
6
|
+
#
|
|
7
|
+
# scripts/backfill_twitter_log_post_no_id.py reconstructs the missing posts rows
|
|
8
|
+
# from skill/logs/twitter-cycle-*.log. It is idempotent: the API dedups on
|
|
9
|
+
# (platform, thread_url), so already-recovered posts no-op.
|
|
10
|
+
#
|
|
11
|
+
# Runs every 30 min via com.m13v.social-twitter-ghost-backfill. We only scan the
|
|
12
|
+
# last 3 days of cycle logs (rolling window) to keep each run fast; the original
|
|
13
|
+
# 64 KB generation_trace outage (2026-05-12..13) was already backfilled once and
|
|
14
|
+
# the cap is now 1 MB, so the steady-state cause is rate-limit / transient only.
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
REPO_DIR="${REPO_DIR:-/Users/matthewdi/social-autoposter}"
|
|
18
|
+
PYTHON_BIN="${PYTHON_BIN:-/opt/homebrew/bin/python3.11}"
|
|
19
|
+
|
|
20
|
+
# macOS date: 3 days ago, YYYY-MM-DD. (date -v is macOS-only; this job is Aqua.)
|
|
21
|
+
SINCE="$(date -v-3d +%Y-%m-%d)"
|
|
22
|
+
|
|
23
|
+
cd "$REPO_DIR"
|
|
24
|
+
exec "$PYTHON_BIN" scripts/backfill_twitter_log_post_no_id.py --since "$SINCE"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Wraps scripts/check_external_pool_depth.py for launchd. Fires at most one
|
|
3
|
+
# email per (project, platform, severity) per 24h via DB-side cooldown.
|
|
4
|
+
set -eu
|
|
5
|
+
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
6
|
+
cd "$REPO_DIR"
|
|
7
|
+
exec /opt/homebrew/bin/python3.11 "$REPO_DIR/scripts/check_external_pool_depth.py"
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-web-chats.sh, poll Postgres for unread web-chat messages and spawn one
|
|
3
|
+
# Claude session per visitor. Called by launchd every 15 seconds.
|
|
4
|
+
#
|
|
5
|
+
# Mirror of ~/fazm/inbox/skill/check-founder-chat.sh: same lock pattern, same
|
|
6
|
+
# claim/cooldown/retry/rate-limit guardrails, same email-summary escalation.
|
|
7
|
+
# The only difference is the data layer: Postgres web_chat_threads /
|
|
8
|
+
# web_chat_messages instead of Firestore founder_chats.
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
# Ensure Homebrew bins (gtimeout, jq) AND the user's npm-global bin (claude)
|
|
13
|
+
# are findable regardless of how the script is invoked. Launchd has these via
|
|
14
|
+
# the plist's PATH; manual / sandboxed shells may not.
|
|
15
|
+
export PATH="/Users/matthewdi/.nvm/versions/node/v20.19.4/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
|
|
16
|
+
|
|
17
|
+
source "$(dirname "$0")/lock.sh"
|
|
18
|
+
acquire_lock "check-web-chats" 60
|
|
19
|
+
|
|
20
|
+
# DB access is HTTP-only via scripts/http_api.py -> s4l.ai /api/v1/web-chat/*.
|
|
21
|
+
# No DATABASE_URL needed here any more.
|
|
22
|
+
|
|
23
|
+
# send-email.js needs RESEND_API_KEY + the analytics node_modules.
|
|
24
|
+
ANALYTICS_ENV="$HOME/analytics/.env.production.local"
|
|
25
|
+
if [ -f "$ANALYTICS_ENV" ]; then
|
|
26
|
+
export RESEND_API_KEY=$(grep '^RESEND_API_KEY=' "$ANALYTICS_ENV" | sed 's/^RESEND_API_KEY=//' | tr -d '"' | tr -d '\\n')
|
|
27
|
+
fi
|
|
28
|
+
export NODE_PATH="$HOME/analytics/node_modules"
|
|
29
|
+
|
|
30
|
+
REPO_DIR="$HOME/social-autoposter"
|
|
31
|
+
SCRIPTS_DIR="$REPO_DIR/scripts"
|
|
32
|
+
LOG_DIR="$REPO_DIR/skill/logs"
|
|
33
|
+
mkdir -p "$LOG_DIR"
|
|
34
|
+
|
|
35
|
+
PYTHON_BIN="${PYTHON_BIN:-/opt/homebrew/bin/python3.11}"
|
|
36
|
+
[ -x "$PYTHON_BIN" ] || PYTHON_BIN="/usr/bin/python3"
|
|
37
|
+
|
|
38
|
+
log() { echo "[$(date +%H:%M:%S)] $*" >> "$LOG_DIR/web-chat.log"; }
|
|
39
|
+
|
|
40
|
+
# Step 1: query Postgres for unread threads.
|
|
41
|
+
CHATS=$("$PYTHON_BIN" "$SCRIPTS_DIR/check_unread_web_chats.py" 2>>"$LOG_DIR/web-chat.log")
|
|
42
|
+
if [ "$CHATS" = "[]" ] || [ -z "$CHATS" ]; then
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
NUM=$(echo "$CHATS" | "$PYTHON_BIN" -c "import json,sys; print(len(json.load(sys.stdin)))")
|
|
47
|
+
|
|
48
|
+
for i in $(seq 0 $((NUM - 1))); do
|
|
49
|
+
THREAD_ID=$(echo "$CHATS" | "$PYTHON_BIN" -c "import json,sys; print(json.load(sys.stdin)[$i]['thread_id'])")
|
|
50
|
+
PROJECT=$(echo "$CHATS" | "$PYTHON_BIN" -c "import json,sys; print(json.load(sys.stdin)[$i]['project'])")
|
|
51
|
+
EMAIL=$(echo "$CHATS" | "$PYTHON_BIN" -c "import json,sys; print(json.load(sys.stdin)[$i].get('visitor_email',''))")
|
|
52
|
+
NAME=$(echo "$CHATS" | "$PYTHON_BIN" -c "import json,sys; d=json.load(sys.stdin)[$i]; print(d.get('visitor_name') or d.get('visitor_email') or 'visitor')")
|
|
53
|
+
UNREAD=$(echo "$CHATS" | "$PYTHON_BIN" -c "import json,sys; print(json.load(sys.stdin)[$i]['unread'])")
|
|
54
|
+
PAGE_URL=$(echo "$CHATS" | "$PYTHON_BIN" -c "import json,sys; print(json.load(sys.stdin)[$i].get('page_url',''))")
|
|
55
|
+
|
|
56
|
+
PID_FILE="/tmp/web-chat-${THREAD_ID}.pid"
|
|
57
|
+
|
|
58
|
+
# Rate-limit circuit breaker (mirror Fazm /tmp/fazm-chat-ratelimit).
|
|
59
|
+
if [ -f "/tmp/web-chat-ratelimit" ]; then
|
|
60
|
+
RL_TS=$(awk '{print $2}' /tmp/web-chat-ratelimit 2>/dev/null || echo "0")
|
|
61
|
+
NOW_TS=$(date +%s)
|
|
62
|
+
if [ $((NOW_TS - RL_TS)) -lt 3600 ]; then
|
|
63
|
+
continue
|
|
64
|
+
else
|
|
65
|
+
rm -f /tmp/web-chat-ratelimit
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Skip if a Claude session is already alive for this thread.
|
|
70
|
+
if [ -f "$PID_FILE" ]; then
|
|
71
|
+
EXISTING_PID=$(cat "$PID_FILE" 2>/dev/null || echo "")
|
|
72
|
+
if [ -n "$EXISTING_PID" ] && kill -0 "$EXISTING_PID" 2>/dev/null; then
|
|
73
|
+
log "Session already active for $PROJECT/$THREAD_ID (pid $EXISTING_PID), skipping"
|
|
74
|
+
continue
|
|
75
|
+
fi
|
|
76
|
+
rm -f "$PID_FILE"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
log "Spawning session for $PROJECT/$THREAD_ID ($EMAIL, $UNREAD unread)"
|
|
80
|
+
|
|
81
|
+
# Cooldown check (mirror claim-chat --check-only).
|
|
82
|
+
if ! "$PYTHON_BIN" "$SCRIPTS_DIR/claim_web_chat.py" "$THREAD_ID" --check-only 2>>"$LOG_DIR/web-chat.log"; then
|
|
83
|
+
log "Thread $THREAD_ID in cooldown, skipping"
|
|
84
|
+
continue
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Claim (resets unread, sets 5-min cooldown).
|
|
88
|
+
"$PYTHON_BIN" "$SCRIPTS_DIR/claim_web_chat.py" "$THREAD_ID" 2>>"$LOG_DIR/web-chat.log" \
|
|
89
|
+
|| log "WARNING: claim failed for $THREAD_ID"
|
|
90
|
+
|
|
91
|
+
# Build prompt (history is dumped from Postgres for freshness).
|
|
92
|
+
HISTORY_JSON=$("$PYTHON_BIN" "$SCRIPTS_DIR/dump_web_chat_history.py" --thread "$THREAD_ID")
|
|
93
|
+
|
|
94
|
+
# Pull this project's config block to give Claude context.
|
|
95
|
+
PROJECT_CFG=$(/opt/homebrew/bin/jq --arg n "$PROJECT" '.projects[] | select(.name==$n)' "$REPO_DIR/config.json" 2>/dev/null || echo "{}")
|
|
96
|
+
|
|
97
|
+
PROMPT_FILE=$(mktemp)
|
|
98
|
+
cat > "$PROMPT_FILE" <<PROMPT_EOF
|
|
99
|
+
Read ~/social-autoposter/skill/WEB-CHAT-SKILL.md for the workflow.
|
|
100
|
+
Read ~/social-autoposter/skill/WEB-CHAT-VOICE.md for tone rules.
|
|
101
|
+
|
|
102
|
+
## Web chat to handle
|
|
103
|
+
|
|
104
|
+
PROJECT: $PROJECT
|
|
105
|
+
THREAD_ID: $THREAD_ID
|
|
106
|
+
VISITOR_EMAIL: $EMAIL
|
|
107
|
+
VISITOR_NAME: $NAME
|
|
108
|
+
PAGE_URL: $PAGE_URL
|
|
109
|
+
UNREAD MESSAGES: $UNREAD
|
|
110
|
+
|
|
111
|
+
## Project config (config.json)
|
|
112
|
+
\`\`\`json
|
|
113
|
+
$PROJECT_CFG
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
## Full conversation history (from Postgres)
|
|
117
|
+
\`\`\`json
|
|
118
|
+
$HISTORY_JSON
|
|
119
|
+
\`\`\`
|
|
120
|
+
|
|
121
|
+
Process this chat now. Follow WEB-CHAT-SKILL.md exactly.
|
|
122
|
+
Remember to remove the PID file /tmp/web-chat-${THREAD_ID}.pid when done.
|
|
123
|
+
PROMPT_EOF
|
|
124
|
+
|
|
125
|
+
SESSION_LOG="$LOG_DIR/web-chat-session-${THREAD_ID}-$(date +%Y%m%d_%H%M%S).log"
|
|
126
|
+
FAIL_COUNT_FILE="/tmp/web-chat-fail-${THREAD_ID}"
|
|
127
|
+
|
|
128
|
+
(
|
|
129
|
+
set +e
|
|
130
|
+
cd "$REPO_DIR"
|
|
131
|
+
echo "[$(date)] Starting Claude session for $PROJECT/$THREAD_ID ($EMAIL)" >> "$SESSION_LOG"
|
|
132
|
+
gtimeout 1200 claude \
|
|
133
|
+
-p "$(cat "$PROMPT_FILE")" \
|
|
134
|
+
--dangerously-skip-permissions \
|
|
135
|
+
>> "$SESSION_LOG" 2>&1
|
|
136
|
+
EXIT_CODE=$?
|
|
137
|
+
echo "[$(date)] Claude exited with code $EXIT_CODE" >> "$SESSION_LOG"
|
|
138
|
+
|
|
139
|
+
if [ $EXIT_CODE -ne 0 ]; then
|
|
140
|
+
echo "[$(date)] WARN: session for $THREAD_ID exited with $EXIT_CODE" >> "$LOG_DIR/web-chat.log"
|
|
141
|
+
|
|
142
|
+
# Detect persistent-error states that won't recover with quick retry:
|
|
143
|
+
# rate limits, credit/billing, auth/quota, account-level issues.
|
|
144
|
+
# All trip the same 1h pause; the next cycle re-tries automatically.
|
|
145
|
+
# Pending threads stay in Postgres (unread_by_founder>0) so nothing is
|
|
146
|
+
# ever lost; the launchd poller picks them up the moment the 1h
|
|
147
|
+
# marker expires. No human notification — the log line is enough.
|
|
148
|
+
PAUSE_PATTERNS='hit your limit|rate limit|rate.limited|too many requests|usage limit|weekly limit|5.hour limit|credit balance|out of credit|insufficient (credit|funds|balance)|payment required|billing|quota exceeded|api[- ]?key|unauthori[sz]ed|forbidden|account.{0,30}(suspend|disabled)|HTTP 401|HTTP 403|HTTP 429|invalid.*x.api.key'
|
|
149
|
+
if grep -qiE "$PAUSE_PATTERNS" "$SESSION_LOG" 2>/dev/null; then
|
|
150
|
+
echo "[$(date)] PERSISTENT ERROR on $THREAD_ID (rate limit / credits / auth), pausing all spawns for 1h" >> "$LOG_DIR/web-chat.log"
|
|
151
|
+
echo "rate_limited $(date +%s)" > "/tmp/web-chat-ratelimit"
|
|
152
|
+
rm -f "$PROMPT_FILE" "$PID_FILE" "$FAIL_COUNT_FILE"
|
|
153
|
+
exit 0
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
FAILS=0
|
|
157
|
+
[ -f "$FAIL_COUNT_FILE" ] && FAILS=$(cat "$FAIL_COUNT_FILE" 2>/dev/null || echo "0")
|
|
158
|
+
FAILS=$((FAILS + 1))
|
|
159
|
+
echo "$FAILS" > "$FAIL_COUNT_FILE"
|
|
160
|
+
|
|
161
|
+
if [ "$FAILS" -ge 3 ]; then
|
|
162
|
+
echo "[$(date)] GIVING UP on $THREAD_ID after $FAILS fails" >> "$LOG_DIR/web-chat.log"
|
|
163
|
+
rm -f "$FAIL_COUNT_FILE"
|
|
164
|
+
# Leave claimed so it stops retrying.
|
|
165
|
+
else
|
|
166
|
+
"$PYTHON_BIN" "$SCRIPTS_DIR/unclaim_web_chat.py" "$THREAD_ID" >> "$LOG_DIR/web-chat.log" 2>&1
|
|
167
|
+
echo "[$(date)] Unclaimed $THREAD_ID (retry $FAILS/3)" >> "$LOG_DIR/web-chat.log"
|
|
168
|
+
fi
|
|
169
|
+
else
|
|
170
|
+
# Claude finished cleanly (replied OR explicitly skipped). Stamp
|
|
171
|
+
# processed_at so the recovery query in check_unread_web_chats.py
|
|
172
|
+
# won't re-flag this thread next cycle. Without this, threads where
|
|
173
|
+
# Claude legitimately skipped (smoke test, off-topic, no useful
|
|
174
|
+
# answer) loop every 5min for 24h, since last_message_sender stays
|
|
175
|
+
# 'visitor' (no agent message inserted on skip).
|
|
176
|
+
"$PYTHON_BIN" "$SCRIPTS_DIR/mark_web_chat_processed.py" "$THREAD_ID" >> "$LOG_DIR/web-chat.log" 2>&1
|
|
177
|
+
rm -f "$FAIL_COUNT_FILE"
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
# No-output guard (silent rate limits sometimes). If Claude exited 0
|
|
181
|
+
# but produced almost no output, treat as a silent failure: unclaim
|
|
182
|
+
# so the next cycle retries via the main unread>0 path. The
|
|
183
|
+
# processed_at stamp above is harmless here because the main SELECT
|
|
184
|
+
# gates on unread_by_founder>0, not on processed_at.
|
|
185
|
+
LINE_COUNT=$(wc -l < "$SESSION_LOG" 2>/dev/null || echo "0")
|
|
186
|
+
if [ "$LINE_COUNT" -le 2 ] && [ "$EXIT_CODE" -eq 0 ]; then
|
|
187
|
+
echo "[$(date)] WARN: $THREAD_ID produced no output, unclaiming" >> "$LOG_DIR/web-chat.log"
|
|
188
|
+
"$PYTHON_BIN" "$SCRIPTS_DIR/unclaim_web_chat.py" "$THREAD_ID" >> "$LOG_DIR/web-chat.log" 2>&1
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
rm -f "$PROMPT_FILE" "$PID_FILE"
|
|
192
|
+
) &
|
|
193
|
+
|
|
194
|
+
CLAUDE_PID=$!
|
|
195
|
+
echo "$CLAUDE_PID" > "$PID_FILE"
|
|
196
|
+
log "Started session for $PROJECT/$THREAD_ID (pid $CLAUDE_PID)"
|
|
197
|
+
done
|
|
198
|
+
|
|
199
|
+
# Trim log to last 2000 lines.
|
|
200
|
+
if [ -f "$LOG_DIR/web-chat.log" ]; then
|
|
201
|
+
tail -2000 "$LOG_DIR/web-chat.log" > "$LOG_DIR/web-chat.log.tmp" 2>/dev/null && \
|
|
202
|
+
mv "$LOG_DIR/web-chat.log.tmp" "$LOG_DIR/web-chat.log" || true
|
|
203
|
+
fi
|