@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,301 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""engage_twitter_helper.py — small CLI wrapper used by skill/engage-twitter.sh
|
|
3
|
+
to replace the six `psql -t -A -c "..."` one-liners the shell used to embed
|
|
4
|
+
inline. Every subcommand prints exactly one value to stdout (string / int /
|
|
5
|
+
JSON) so bash can capture it with $(...) without changing shape.
|
|
6
|
+
|
|
7
|
+
Subcommands:
|
|
8
|
+
reset-stuck-replies
|
|
9
|
+
-> POST /api/v1/replies/reset-stuck { platform:'x', older_than_hours:2 }
|
|
10
|
+
-> prints the integer reset_count
|
|
11
|
+
pending-count
|
|
12
|
+
-> GET /api/v1/replies/counts?platform=x
|
|
13
|
+
-> prints the pending count
|
|
14
|
+
reply-counts
|
|
15
|
+
-> GET /api/v1/replies/counts?platform=x
|
|
16
|
+
-> prints JSON {pending, replied, skipped}
|
|
17
|
+
pending-data --batch-size N
|
|
18
|
+
-> GET /api/v1/replies/next-pending?platform=x&limit=N
|
|
19
|
+
then reshape to the legacy json_agg() shape engage-twitter.sh's
|
|
20
|
+
prompt-template expects:
|
|
21
|
+
[{id, platform, their_author, their_content, their_comment_url,
|
|
22
|
+
their_comment_id, depth, thread_title, thread_url, our_content,
|
|
23
|
+
our_url, is_our_original_post, project_name}, ...]
|
|
24
|
+
post-reset
|
|
25
|
+
-> POST /api/v1/replies/reset-stuck { platform:'x', older_than_hours:0 }
|
|
26
|
+
-> prints reset_count
|
|
27
|
+
active-campaign
|
|
28
|
+
-> GET /api/v1/campaigns?platform=twitter&has_suffix=true
|
|
29
|
+
&with_budget_remaining=true&status=active&limit=1
|
|
30
|
+
-> prints JSON {id, suffix, sample_rate} or {} when none active
|
|
31
|
+
|
|
32
|
+
Migrated 2026-05-18: the bash used to embed six raw `psql` queries against
|
|
33
|
+
Postgres for this engage loop; this helper replaces them all with HTTP API
|
|
34
|
+
calls and keeps the bash side free of DATABASE_URL handling for the engage
|
|
35
|
+
pipeline.
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import argparse
|
|
40
|
+
import json
|
|
41
|
+
import os
|
|
42
|
+
import sys
|
|
43
|
+
|
|
44
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
45
|
+
from http_api import api_get, api_post # noqa: E402
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def cmd_reset_stuck_replies() -> int:
|
|
49
|
+
resp = api_post(
|
|
50
|
+
"/api/v1/replies/reset-stuck",
|
|
51
|
+
{"platform": "x", "older_than_hours": 2},
|
|
52
|
+
)
|
|
53
|
+
data = resp.get("data") or {}
|
|
54
|
+
print(int(data.get("reset_count") or 0))
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def cmd_post_reset() -> int:
|
|
59
|
+
# 0-hour window resets every 'processing' reply we have right now.
|
|
60
|
+
# Mirrors the engage-twitter.sh "leftover after subprocess exit" sweep.
|
|
61
|
+
resp = api_post(
|
|
62
|
+
"/api/v1/replies/reset-stuck",
|
|
63
|
+
{"platform": "x", "older_than_hours": 1},
|
|
64
|
+
)
|
|
65
|
+
data = resp.get("data") or {}
|
|
66
|
+
print(int(data.get("reset_count") or 0))
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _counts_dict() -> dict[str, int]:
|
|
71
|
+
"""Reshape /api/v1/replies/counts into the flat {pending, replied,
|
|
72
|
+
skipped, processing} dict our callers expect.
|
|
73
|
+
|
|
74
|
+
Prefers `eligible_counts` (JOIN-aware: matches what /next-pending
|
|
75
|
+
actually surfaces) over the raw `counts` field. The two diverge when
|
|
76
|
+
truly-orphan rows exist (post_id pointing at a deleted post AND no
|
|
77
|
+
mention_id / parent_reply_id fallback) — historically the raw count
|
|
78
|
+
misled engage-twitter's early-skip gate into burning the
|
|
79
|
+
twitter-browser lock for the full Phase B window finding nothing.
|
|
80
|
+
Falls back to raw `counts` if the deploy doesn't yet expose
|
|
81
|
+
`eligible_counts` (pre-2026-05-26 vintage).
|
|
82
|
+
"""
|
|
83
|
+
resp = api_get(
|
|
84
|
+
"/api/v1/replies/counts",
|
|
85
|
+
query={"platform": "x"},
|
|
86
|
+
)
|
|
87
|
+
data = resp.get("data") or {}
|
|
88
|
+
rows = data.get("eligible_counts") or data.get("counts") or []
|
|
89
|
+
out: dict[str, int] = {}
|
|
90
|
+
for r in rows:
|
|
91
|
+
s = r.get("status")
|
|
92
|
+
c = r.get("count")
|
|
93
|
+
if s is None:
|
|
94
|
+
continue
|
|
95
|
+
try:
|
|
96
|
+
out[str(s)] = int(c or 0)
|
|
97
|
+
except (TypeError, ValueError):
|
|
98
|
+
out[str(s)] = 0
|
|
99
|
+
return out
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def cmd_pending_count() -> int:
|
|
103
|
+
counts = _counts_dict()
|
|
104
|
+
print(int(counts.get("pending") or 0))
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def cmd_reply_counts() -> int:
|
|
109
|
+
counts = _counts_dict()
|
|
110
|
+
out = {
|
|
111
|
+
"pending": int(counts.get("pending") or 0),
|
|
112
|
+
"replied": int(counts.get("replied") or 0),
|
|
113
|
+
"skipped": int(counts.get("skipped") or 0),
|
|
114
|
+
}
|
|
115
|
+
json.dump(out, sys.stdout, separators=(",", ":"))
|
|
116
|
+
sys.stdout.write("\n")
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _render_media_block(media) -> str:
|
|
121
|
+
"""Render replies.their_media ([{url,alt,type}]) into a short, self-titled
|
|
122
|
+
text block for the Phase B prompt (2026-06-03 thread-media feature). Empty
|
|
123
|
+
string when the comment had no media (or media was never captured), so it
|
|
124
|
+
stays invisible in the embedded JSON for text-only comments.
|
|
125
|
+
"""
|
|
126
|
+
if not isinstance(media, list) or not media:
|
|
127
|
+
return ""
|
|
128
|
+
lines = []
|
|
129
|
+
for it in media:
|
|
130
|
+
if not isinstance(it, dict):
|
|
131
|
+
continue
|
|
132
|
+
t = (it.get("type") or "media").strip()
|
|
133
|
+
alt = (it.get("alt") or "").strip()
|
|
134
|
+
url = (it.get("url") or "").strip()
|
|
135
|
+
alt_part = f'"{alt}"' if alt else "[no description]"
|
|
136
|
+
lines.append(f" - {t}: {alt_part} ({url})")
|
|
137
|
+
if not lines:
|
|
138
|
+
return ""
|
|
139
|
+
return (
|
|
140
|
+
"## Media in the comment you are replying to\n"
|
|
141
|
+
"React to what it VISUALLY shows, not just the text. "
|
|
142
|
+
"[no description] = no alt-text; infer from the comment + media type.\n"
|
|
143
|
+
+ "\n".join(lines)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def cmd_pending_data(batch_size: int) -> int:
|
|
148
|
+
try:
|
|
149
|
+
from account_resolver import resolve as _resolve_account # noqa: WPS433
|
|
150
|
+
our_account = _resolve_account("twitter")
|
|
151
|
+
except Exception:
|
|
152
|
+
our_account = None
|
|
153
|
+
query = {"platform": "x", "limit": batch_size}
|
|
154
|
+
if our_account:
|
|
155
|
+
query["our_account"] = our_account
|
|
156
|
+
resp = api_get(
|
|
157
|
+
"/api/v1/replies/next-pending",
|
|
158
|
+
query=query,
|
|
159
|
+
)
|
|
160
|
+
rows = (resp.get("data") or {}).get("replies") or []
|
|
161
|
+
|
|
162
|
+
# Enrich each row with a per-counterparty history block (DM cross-thread
|
|
163
|
+
# + public-reply history) via the shared counterparty_history module.
|
|
164
|
+
# The block is self-titled ("## Prior history with @author") and lands
|
|
165
|
+
# inline in PENDING_DATA so the Phase B prompt picks it up without any
|
|
166
|
+
# change to the shell-side prompt template.
|
|
167
|
+
#
|
|
168
|
+
# Capped to ENRICH_TOP_N because the API list is priority-ordered
|
|
169
|
+
# (our_thread first, then discovered_at ASC) and the Phase B Claude
|
|
170
|
+
# session rarely processes more than ~50 items before gtimeout fires.
|
|
171
|
+
# Beyond the cap we leave counterparty_history_block empty; if a row
|
|
172
|
+
# falls past the cap and IS reached on a later cycle, it'll be in the
|
|
173
|
+
# top slot then and get enriched.
|
|
174
|
+
ENRICH_TOP_N = 60
|
|
175
|
+
history_blocks = [""] * len(rows)
|
|
176
|
+
try:
|
|
177
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
178
|
+
from counterparty_history import get_counterparty_history_block
|
|
179
|
+
|
|
180
|
+
def _enrich(r):
|
|
181
|
+
author = r.get("their_author")
|
|
182
|
+
if not author:
|
|
183
|
+
return ""
|
|
184
|
+
try:
|
|
185
|
+
_disengage, block = get_counterparty_history_block(
|
|
186
|
+
platform="x",
|
|
187
|
+
author=author,
|
|
188
|
+
current_post_id=r.get("post_id"),
|
|
189
|
+
current_reply_id=r.get("id"),
|
|
190
|
+
)
|
|
191
|
+
return block or ""
|
|
192
|
+
except Exception as e:
|
|
193
|
+
print(
|
|
194
|
+
f"[engage_twitter_helper] counterparty_history failed "
|
|
195
|
+
f"for @{author}: {e}",
|
|
196
|
+
file=sys.stderr,
|
|
197
|
+
)
|
|
198
|
+
return ""
|
|
199
|
+
|
|
200
|
+
top_rows = rows[:ENRICH_TOP_N]
|
|
201
|
+
with ThreadPoolExecutor(max_workers=8) as ex:
|
|
202
|
+
for idx, block in enumerate(ex.map(_enrich, top_rows)):
|
|
203
|
+
history_blocks[idx] = block
|
|
204
|
+
non_empty = sum(1 for b in history_blocks if b)
|
|
205
|
+
print(
|
|
206
|
+
f"[engage_twitter_helper] counterparty_history enriched "
|
|
207
|
+
f"{len(top_rows)}/{len(rows)} rows ({non_empty} with non-empty block)",
|
|
208
|
+
file=sys.stderr,
|
|
209
|
+
)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
print(
|
|
212
|
+
f"[engage_twitter_helper] enrichment phase failed "
|
|
213
|
+
f"(continuing without history): {e}",
|
|
214
|
+
file=sys.stderr,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
out = []
|
|
218
|
+
for r, history_block in zip(rows, history_blocks):
|
|
219
|
+
out.append({
|
|
220
|
+
"id": r.get("id"),
|
|
221
|
+
"platform": r.get("platform"),
|
|
222
|
+
"their_author": r.get("their_author"),
|
|
223
|
+
"their_content": r.get("their_content"),
|
|
224
|
+
"their_comment_url": r.get("their_comment_url"),
|
|
225
|
+
"their_comment_id": r.get("their_comment_id"),
|
|
226
|
+
"depth": r.get("depth"),
|
|
227
|
+
"thread_title": r.get("thread_title"),
|
|
228
|
+
"thread_url": r.get("thread_url"),
|
|
229
|
+
"our_content": r.get("our_content"),
|
|
230
|
+
"our_url": r.get("our_url"),
|
|
231
|
+
"is_our_original_post": int(r.get("is_our_original_post") or 0),
|
|
232
|
+
"project_name": r.get("project_name"),
|
|
233
|
+
"counterparty_history_block": history_block,
|
|
234
|
+
"their_media_block": _render_media_block(r.get("their_media")),
|
|
235
|
+
})
|
|
236
|
+
# json_agg(...) returns null when the array is empty; engage-twitter.sh's
|
|
237
|
+
# downstream prompt-template expects an empty array instead, which is
|
|
238
|
+
# easier to embed verbatim.
|
|
239
|
+
json.dump(out, sys.stdout, separators=(",", ":"))
|
|
240
|
+
sys.stdout.write("\n")
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def cmd_active_campaign() -> int:
|
|
245
|
+
resp = api_get(
|
|
246
|
+
"/api/v1/campaigns",
|
|
247
|
+
query={
|
|
248
|
+
"status": "active",
|
|
249
|
+
"platform": "twitter",
|
|
250
|
+
"has_suffix": "true",
|
|
251
|
+
"with_budget_remaining": "true",
|
|
252
|
+
"limit": 1,
|
|
253
|
+
},
|
|
254
|
+
)
|
|
255
|
+
rows = (resp.get("data") or {}).get("campaigns") or []
|
|
256
|
+
if not rows:
|
|
257
|
+
sys.stdout.write("{}\n")
|
|
258
|
+
return 0
|
|
259
|
+
r = rows[0]
|
|
260
|
+
out = {
|
|
261
|
+
"id": r.get("id"),
|
|
262
|
+
"suffix": r.get("suffix"),
|
|
263
|
+
"sample_rate": float(r.get("sample_rate") or 1.0),
|
|
264
|
+
}
|
|
265
|
+
json.dump(out, sys.stdout, separators=(",", ":"))
|
|
266
|
+
sys.stdout.write("\n")
|
|
267
|
+
return 0
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def main() -> int:
|
|
271
|
+
ap = argparse.ArgumentParser(description="Helper for engage-twitter.sh")
|
|
272
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
273
|
+
|
|
274
|
+
sub.add_parser("reset-stuck-replies")
|
|
275
|
+
sub.add_parser("pending-count")
|
|
276
|
+
sub.add_parser("reply-counts")
|
|
277
|
+
sub.add_parser("post-reset")
|
|
278
|
+
sub.add_parser("active-campaign")
|
|
279
|
+
|
|
280
|
+
p_pending = sub.add_parser("pending-data")
|
|
281
|
+
p_pending.add_argument("--batch-size", type=int, default=500)
|
|
282
|
+
|
|
283
|
+
args = ap.parse_args()
|
|
284
|
+
|
|
285
|
+
if args.cmd == "reset-stuck-replies":
|
|
286
|
+
return cmd_reset_stuck_replies()
|
|
287
|
+
if args.cmd == "pending-count":
|
|
288
|
+
return cmd_pending_count()
|
|
289
|
+
if args.cmd == "reply-counts":
|
|
290
|
+
return cmd_reply_counts()
|
|
291
|
+
if args.cmd == "post-reset":
|
|
292
|
+
return cmd_post_reset()
|
|
293
|
+
if args.cmd == "active-campaign":
|
|
294
|
+
return cmd_active_campaign()
|
|
295
|
+
if args.cmd == "pending-data":
|
|
296
|
+
return cmd_pending_data(args.batch_size)
|
|
297
|
+
return 1
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
if __name__ == "__main__":
|
|
301
|
+
sys.exit(main())
|