@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
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""dm_db_update.py — single-row PATCH helper for the `dms` table.
|
|
3
|
+
|
|
4
|
+
Created 2026-05-18 as the replacement for the three inline
|
|
5
|
+
`psql "$DATABASE_URL" -c "UPDATE dms SET ..."` lines the dm-outreach-*
|
|
6
|
+
shell pipelines used to embed in their Claude prompts. The LLM is told to
|
|
7
|
+
shell out to this script instead of psql so all DB writes route through
|
|
8
|
+
/api/v1/dms/:id and we keep the credentials surface inside the helper.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python3 scripts/dm_db_update.py --dm-id N \
|
|
12
|
+
[--status pending|sent|error|skipped|...] \
|
|
13
|
+
[--skip-reason TEXT] \
|
|
14
|
+
[--claude-session-id UUID]
|
|
15
|
+
|
|
16
|
+
At least one of --status / --skip-reason / --claude-session-id is
|
|
17
|
+
required. Status and skip_reason can be set independently (the PATCH
|
|
18
|
+
route uses COALESCE for every field, so omitted fields stay unchanged).
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
27
|
+
from http_api import api_patch # noqa: E402
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main() -> int:
|
|
31
|
+
ap = argparse.ArgumentParser()
|
|
32
|
+
ap.add_argument("--dm-id", type=int, required=True)
|
|
33
|
+
ap.add_argument("--status")
|
|
34
|
+
ap.add_argument("--skip-reason")
|
|
35
|
+
ap.add_argument("--claude-session-id")
|
|
36
|
+
args = ap.parse_args()
|
|
37
|
+
|
|
38
|
+
body: dict = {}
|
|
39
|
+
if args.status:
|
|
40
|
+
body["status"] = args.status
|
|
41
|
+
if args.skip_reason:
|
|
42
|
+
body["skip_reason"] = args.skip_reason
|
|
43
|
+
if args.claude_session_id:
|
|
44
|
+
body["claude_session_id"] = args.claude_session_id
|
|
45
|
+
|
|
46
|
+
if not body:
|
|
47
|
+
print(
|
|
48
|
+
"dm_db_update: nothing to update; pass at least --status, "
|
|
49
|
+
"--skip-reason, or --claude-session-id",
|
|
50
|
+
file=sys.stderr,
|
|
51
|
+
)
|
|
52
|
+
return 1
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
resp = api_patch(f"/api/v1/dms/{args.dm_id}", body)
|
|
56
|
+
except SystemExit as e:
|
|
57
|
+
print(f"dm_db_update: PATCH /api/v1/dms/{args.dm_id} failed: {e}", file=sys.stderr)
|
|
58
|
+
return 1
|
|
59
|
+
|
|
60
|
+
dm = (resp.get("data") or {}).get("dm") or {}
|
|
61
|
+
print(
|
|
62
|
+
f"dm_db_update: dm #{args.dm_id} status={dm.get('status')!r} "
|
|
63
|
+
f"skip_reason={dm.get('skip_reason')!r}",
|
|
64
|
+
)
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
sys.exit(main())
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""HTTP-only stdout shims for the psql one-liners in skill/engage-dm-replies.sh.
|
|
3
|
+
|
|
4
|
+
The engage pipeline used to embed raw `psql "$DATABASE_URL" -t -A -c "..."`
|
|
5
|
+
calls for its read-side gates and end-of-run summary. The direct-Postgres lane
|
|
6
|
+
was removed 2026-06-01; DATABASE_URL is deliberately ignored, no DB, no
|
|
7
|
+
fallback. Each subcommand here calls the s4l.ai HTTP API (scripts/http_api.py)
|
|
8
|
+
and prints EXACTLY what the corresponding psql call printed, so the shell
|
|
9
|
+
parsing around it (json.load, `tr '|' ' '`, integer compares) is unchanged.
|
|
10
|
+
|
|
11
|
+
Subcommands (each maps 1:1 to a former psql call):
|
|
12
|
+
pending --platform X --limit 30 -> PENDING_CONVOS: JSON array, or 'null'
|
|
13
|
+
needs-reply --platform X -> needs_reply_count_for: integer
|
|
14
|
+
run-counts --platform X --since N -> dm_counts_for: 'POSTED STALE'
|
|
15
|
+
summary -> DM_SUMMARY: json object
|
|
16
|
+
reddit-authors -> KNOWN_REDDIT_AUTHORS: 'a, b, c'
|
|
17
|
+
reddit-campaign-suffix -> REDDIT_CAMPAIGN_SUFFIX_LITERAL
|
|
18
|
+
reddit-campaign-sample-rate -> REDDIT_CAMPAIGN_SAMPLE_RATE
|
|
19
|
+
flagged-count -> FLAGGED_COUNT: integer
|
|
20
|
+
|
|
21
|
+
All endpoints live under /api/v1/dms/engage except the campaign subcommands
|
|
22
|
+
(/api/v1/campaigns) and flagged-count (/api/v1/dms/flagged).
|
|
23
|
+
"""
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
30
|
+
from http_api import api_get
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _data(resp):
|
|
34
|
+
return (resp or {}).get("data") or {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def cmd_pending(args):
|
|
38
|
+
resp = api_get(
|
|
39
|
+
"/api/v1/dms/engage",
|
|
40
|
+
query={"mode": "pending", "platform": args.platform or "", "limit": args.limit},
|
|
41
|
+
)
|
|
42
|
+
rows = _data(resp).get("rows")
|
|
43
|
+
# Mirror psql's `json_agg(...) -> NULL when empty` which the shell echoed as
|
|
44
|
+
# the literal string 'null'.
|
|
45
|
+
if not rows:
|
|
46
|
+
print("null")
|
|
47
|
+
else:
|
|
48
|
+
print(json.dumps(rows))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def cmd_needs_reply(args):
|
|
52
|
+
resp = api_get(
|
|
53
|
+
"/api/v1/dms/engage",
|
|
54
|
+
query={"mode": "needs_reply", "platform": args.platform or ""},
|
|
55
|
+
)
|
|
56
|
+
print(_data(resp).get("count", 0))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def cmd_run_counts(args):
|
|
60
|
+
resp = api_get(
|
|
61
|
+
"/api/v1/dms/engage",
|
|
62
|
+
query={"mode": "run_counts", "platform": args.platform or "", "since": args.since},
|
|
63
|
+
)
|
|
64
|
+
d = _data(resp)
|
|
65
|
+
# psql printed 'posted|stale' then the shell did `tr '|' ' '`; emit the
|
|
66
|
+
# already-split form so `read -r POSTED STALE` works directly.
|
|
67
|
+
print(f"{d.get('posted', 0)} {d.get('stale', 0)}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cmd_summary(_args):
|
|
71
|
+
resp = api_get("/api/v1/dms/engage", query={"mode": "summary"})
|
|
72
|
+
summary = _data(resp).get("summary") or {}
|
|
73
|
+
# Match the json_build_object the shell logs verbatim.
|
|
74
|
+
print(json.dumps(summary))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cmd_reddit_authors(_args):
|
|
78
|
+
resp = api_get("/api/v1/dms/engage", query={"mode": "reddit_authors"})
|
|
79
|
+
print(_data(resp).get("authors") or "")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _active_reddit_campaign():
|
|
83
|
+
"""First active reddit campaign with budget remaining + a non-empty suffix.
|
|
84
|
+
|
|
85
|
+
Mirrors the two REDDIT_CAMPAIGN_* psql queries: status='active',
|
|
86
|
+
platforms includes reddit, max_posts_total set AND posts_made < it,
|
|
87
|
+
suffix non-empty, ORDER BY id LIMIT 1.
|
|
88
|
+
"""
|
|
89
|
+
resp = api_get(
|
|
90
|
+
"/api/v1/campaigns",
|
|
91
|
+
query={
|
|
92
|
+
"status": "active",
|
|
93
|
+
"platform": "reddit",
|
|
94
|
+
"has_suffix": "true",
|
|
95
|
+
"with_budget_remaining": "true",
|
|
96
|
+
"limit": 500,
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
rows = _data(resp).get("campaigns") or []
|
|
100
|
+
for r in rows: # already ORDER BY id ASC server-side
|
|
101
|
+
max_total = r.get("max_posts_total")
|
|
102
|
+
posts_made = r.get("posts_made") or 0
|
|
103
|
+
suffix = r.get("suffix")
|
|
104
|
+
if max_total is None or posts_made >= max_total:
|
|
105
|
+
continue
|
|
106
|
+
if not suffix:
|
|
107
|
+
continue
|
|
108
|
+
return r
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def cmd_reddit_campaign_suffix(_args):
|
|
113
|
+
c = _active_reddit_campaign()
|
|
114
|
+
# psql piped through `tr -d '\n'`; print with no trailing newline.
|
|
115
|
+
sys.stdout.write(c.get("suffix") if c else "")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cmd_reddit_campaign_sample_rate(_args):
|
|
119
|
+
c = _active_reddit_campaign()
|
|
120
|
+
if not c:
|
|
121
|
+
sys.stdout.write("")
|
|
122
|
+
return
|
|
123
|
+
rate = c.get("sample_rate")
|
|
124
|
+
sys.stdout.write("1.000" if rate is None else str(rate))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def cmd_flagged_count(_args):
|
|
128
|
+
resp = api_get("/api/v1/dms/flagged")
|
|
129
|
+
print(_data(resp).get("count", 0))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main():
|
|
133
|
+
p = argparse.ArgumentParser()
|
|
134
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
135
|
+
|
|
136
|
+
sp = sub.add_parser("pending")
|
|
137
|
+
sp.add_argument("--platform", default="")
|
|
138
|
+
sp.add_argument("--limit", type=int, default=30)
|
|
139
|
+
sp.set_defaults(func=cmd_pending)
|
|
140
|
+
|
|
141
|
+
sp = sub.add_parser("needs-reply")
|
|
142
|
+
sp.add_argument("--platform", default="")
|
|
143
|
+
sp.set_defaults(func=cmd_needs_reply)
|
|
144
|
+
|
|
145
|
+
sp = sub.add_parser("run-counts")
|
|
146
|
+
sp.add_argument("--platform", default="")
|
|
147
|
+
sp.add_argument("--since", type=int, required=True)
|
|
148
|
+
sp.set_defaults(func=cmd_run_counts)
|
|
149
|
+
|
|
150
|
+
sub.add_parser("summary").set_defaults(func=cmd_summary)
|
|
151
|
+
sub.add_parser("reddit-authors").set_defaults(func=cmd_reddit_authors)
|
|
152
|
+
sub.add_parser("reddit-campaign-suffix").set_defaults(func=cmd_reddit_campaign_suffix)
|
|
153
|
+
sub.add_parser("reddit-campaign-sample-rate").set_defaults(func=cmd_reddit_campaign_sample_rate)
|
|
154
|
+
sub.add_parser("flagged-count").set_defaults(func=cmd_flagged_count)
|
|
155
|
+
|
|
156
|
+
args = p.parse_args()
|
|
157
|
+
args.func(args)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
main()
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""dm_outreach_helper.py — shell-friendly entrypoints for the dm-outreach
|
|
3
|
+
{reddit,twitter,linkedin}.sh pipelines that used to inline `psql` calls.
|
|
4
|
+
|
|
5
|
+
Subcommands (all route through /api/v1/dms* on the website):
|
|
6
|
+
|
|
7
|
+
count --platform reddit --status pending
|
|
8
|
+
-> prints integer count to stdout (one line). Used by the bash
|
|
9
|
+
script's "DM_PENDING / SENT / STILL_PENDING" sentinels.
|
|
10
|
+
|
|
11
|
+
outreach-queue --platform reddit
|
|
12
|
+
-> prints JSON shape matching the legacy
|
|
13
|
+
`psql ... "SELECT json_agg(q) FROM ... JOIN replies ... JOIN posts ..."`
|
|
14
|
+
query: an array of dms rows joined with reply + post + a 60-day
|
|
15
|
+
other_engagement summary per author. Output is exactly the same
|
|
16
|
+
JSON the LLM prompt expects (DM_DATA variable in dm-outreach-*.sh).
|
|
17
|
+
|
|
18
|
+
patch --id 123 --status error --skip-reason send_unverified
|
|
19
|
+
-> PATCH /api/v1/dms/123. Replaces the bash-embedded
|
|
20
|
+
`psql ... "UPDATE dms SET status=..., skip_reason=..."` blocks
|
|
21
|
+
(the ones the LLM is told to run). Supports any combo of
|
|
22
|
+
--status / --skip-reason / --claude-session-id.
|
|
23
|
+
|
|
24
|
+
(NOTE: this still flips status freely. dm_send_log.py is the only
|
|
25
|
+
path that's allowed to set status='sent' with verification — DO
|
|
26
|
+
NOT use this `patch` subcommand to mark a DM as sent. The legacy
|
|
27
|
+
bash script's prompt was already careful about this; we preserve
|
|
28
|
+
that constraint.)
|
|
29
|
+
|
|
30
|
+
This script intentionally does NOT touch dm_conversation.py or the dms
|
|
31
|
+
DB schema directly. Everything goes through HTTP routes, no psycopg2.
|
|
32
|
+
"""
|
|
33
|
+
import argparse
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
import sys
|
|
37
|
+
|
|
38
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
39
|
+
from http_api import api_get, api_patch
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _cmd_count(args):
|
|
43
|
+
query = {
|
|
44
|
+
"platform": args.platform,
|
|
45
|
+
"status": args.status,
|
|
46
|
+
"count_only": "true",
|
|
47
|
+
}
|
|
48
|
+
if args.target_project:
|
|
49
|
+
query["target_project"] = args.target_project
|
|
50
|
+
resp = api_get("/api/v1/dms", query=query)
|
|
51
|
+
data = (resp or {}).get("data") or {}
|
|
52
|
+
print(int(data.get("count") or 0))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _cmd_outreach_queue(args):
|
|
56
|
+
query = {
|
|
57
|
+
"platform": args.platform,
|
|
58
|
+
"status": args.status,
|
|
59
|
+
"limit": args.limit,
|
|
60
|
+
"other_engagement_days": args.other_engagement_days,
|
|
61
|
+
}
|
|
62
|
+
resp = api_get("/api/v1/dms/outreach-queue", query=query)
|
|
63
|
+
data = (resp or {}).get("data") or {}
|
|
64
|
+
rows = data.get("rows") or []
|
|
65
|
+
# Mirror the legacy `SELECT json_agg(q) FROM (...) q;` output shape.
|
|
66
|
+
# The psql command returned a single JSON array (or empty string when
|
|
67
|
+
# zero rows). The LLM prompt expects an array literal it can read
|
|
68
|
+
# directly. Print [] when empty to match.
|
|
69
|
+
json.dump(rows, sys.stdout)
|
|
70
|
+
print("", file=sys.stdout)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _cmd_patch(args):
|
|
74
|
+
if args.status == "sent":
|
|
75
|
+
# status='sent' must go through scripts/dm_send_log.py (verified
|
|
76
|
+
# outbound path). Anything else (error, skipped, queued, ...) is
|
|
77
|
+
# fine to flip from here.
|
|
78
|
+
print(
|
|
79
|
+
"ERROR: dm_outreach_helper.py patch refuses to set status=sent. "
|
|
80
|
+
"Use scripts/dm_send_log.py with --verified instead.",
|
|
81
|
+
file=sys.stderr,
|
|
82
|
+
)
|
|
83
|
+
sys.exit(2)
|
|
84
|
+
|
|
85
|
+
body: dict = {}
|
|
86
|
+
if args.status:
|
|
87
|
+
body["status"] = args.status
|
|
88
|
+
if args.skip_reason is not None:
|
|
89
|
+
body["skip_reason"] = args.skip_reason
|
|
90
|
+
if args.claude_session_id:
|
|
91
|
+
body["claude_session_id"] = args.claude_session_id
|
|
92
|
+
if args.conversation_status:
|
|
93
|
+
body["conversation_status"] = args.conversation_status
|
|
94
|
+
|
|
95
|
+
if not body:
|
|
96
|
+
print("ERROR: nothing to patch (no --status / --skip-reason / ...)",
|
|
97
|
+
file=sys.stderr)
|
|
98
|
+
sys.exit(2)
|
|
99
|
+
|
|
100
|
+
resp = api_patch(f"/api/v1/dms/{args.id}", body)
|
|
101
|
+
data = (resp or {}).get("data") or {}
|
|
102
|
+
dm = data.get("dm")
|
|
103
|
+
if dm:
|
|
104
|
+
print(f"PATCHED dm_id={dm.get('id')} status={dm.get('status')} "
|
|
105
|
+
f"skip_reason={dm.get('skip_reason')}")
|
|
106
|
+
else:
|
|
107
|
+
# Route returned no body (shouldn't happen on 200) — emit raw resp.
|
|
108
|
+
json.dump(resp, sys.stdout)
|
|
109
|
+
print("", file=sys.stdout)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main():
|
|
113
|
+
p = argparse.ArgumentParser(description=__doc__,
|
|
114
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
115
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
116
|
+
|
|
117
|
+
pc = sub.add_parser("count", help="Print COUNT(*) for filtered dms")
|
|
118
|
+
pc.add_argument("--platform", required=True)
|
|
119
|
+
pc.add_argument("--status", default="pending")
|
|
120
|
+
pc.add_argument("--target-project", default=None)
|
|
121
|
+
pc.set_defaults(func=_cmd_count)
|
|
122
|
+
|
|
123
|
+
pq = sub.add_parser("outreach-queue",
|
|
124
|
+
help="Emit the join'd DM/reply/post JSON for the LLM prompt")
|
|
125
|
+
pq.add_argument("--platform", required=True)
|
|
126
|
+
pq.add_argument("--status", default="pending")
|
|
127
|
+
pq.add_argument("--limit", type=int, default=50)
|
|
128
|
+
pq.add_argument("--other-engagement-days", type=int, default=60)
|
|
129
|
+
pq.set_defaults(func=_cmd_outreach_queue)
|
|
130
|
+
|
|
131
|
+
pp = sub.add_parser("patch",
|
|
132
|
+
help="PATCH a dms row (status / skip_reason / etc.)")
|
|
133
|
+
pp.add_argument("--id", required=True, type=int)
|
|
134
|
+
pp.add_argument("--status", default=None,
|
|
135
|
+
help="New status (NOT 'sent' — use dm_send_log.py for that).")
|
|
136
|
+
pp.add_argument("--skip-reason", default=None,
|
|
137
|
+
help="Reason string (e.g. 'send_unverified', 'reddit_browser_busy').")
|
|
138
|
+
pp.add_argument("--conversation-status", default=None)
|
|
139
|
+
pp.add_argument("--claude-session-id", default=None)
|
|
140
|
+
pp.set_defaults(func=_cmd_patch)
|
|
141
|
+
|
|
142
|
+
args = p.parse_args()
|
|
143
|
+
args.func(args)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
main()
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""dm_outreach_twitter_helper.py — small CLI wrapper used by
|
|
3
|
+
skill/dm-outreach-twitter.sh to replace the four direct `psql` one-liners
|
|
4
|
+
the script used to embed inline (pending count, outreach JSON aggregation,
|
|
5
|
+
MCP-failure recovery sweep, sent/still-pending summary counts).
|
|
6
|
+
|
|
7
|
+
Subcommands:
|
|
8
|
+
pending-count
|
|
9
|
+
-> GET /api/v1/dms/counts?platform=twitter (canonicalises to 'x')
|
|
10
|
+
-> prints the integer pending count
|
|
11
|
+
|
|
12
|
+
outreach-queue
|
|
13
|
+
-> GET /api/v1/dms/outreach-queue?platform=twitter&status=pending
|
|
14
|
+
-> prints the rows as a JSON ARRAY (mirrors the legacy
|
|
15
|
+
`SELECT json_agg(q) FROM (...) q` shape the bash prompt embeds)
|
|
16
|
+
|
|
17
|
+
recover-mcp --session-id UUID
|
|
18
|
+
-> POST /api/v1/dms/recover-mcp-failures { platform, claude_session_id }
|
|
19
|
+
-> prints the recovered_count integer
|
|
20
|
+
|
|
21
|
+
summary
|
|
22
|
+
-> GET /api/v1/dms/counts?platform=twitter
|
|
23
|
+
-> prints "<sent> <still_pending>" so the legacy two-variable capture
|
|
24
|
+
keeps working (SENT/STILL_PENDING in dm-outreach-twitter.sh).
|
|
25
|
+
|
|
26
|
+
Migrated 2026-05-18: removes 4 direct psql calls from
|
|
27
|
+
skill/dm-outreach-twitter.sh. The dms table stores Twitter rows with
|
|
28
|
+
platform='x' (scan_dm_candidates.py:219-220 normalises 'twitter' → 'x'
|
|
29
|
+
before INSERT); routes accept either form and the canonicalDmPlatform
|
|
30
|
+
helper rewrites the WHERE clauses uniformly.
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import argparse
|
|
35
|
+
import json
|
|
36
|
+
import os
|
|
37
|
+
import sys
|
|
38
|
+
|
|
39
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
40
|
+
from http_api import api_get, api_post # noqa: E402
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _counts_dict() -> dict[str, int]:
|
|
44
|
+
resp = api_get(
|
|
45
|
+
"/api/v1/dms/counts",
|
|
46
|
+
query={"platform": "twitter"},
|
|
47
|
+
)
|
|
48
|
+
rows = (resp.get("data") or {}).get("counts") or []
|
|
49
|
+
out: dict[str, int] = {}
|
|
50
|
+
for r in rows:
|
|
51
|
+
s = r.get("status")
|
|
52
|
+
if s is None:
|
|
53
|
+
continue
|
|
54
|
+
try:
|
|
55
|
+
out[str(s)] = int(r.get("count") or 0)
|
|
56
|
+
except (TypeError, ValueError):
|
|
57
|
+
out[str(s)] = 0
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def cmd_pending_count() -> int:
|
|
62
|
+
counts = _counts_dict()
|
|
63
|
+
sys.stdout.write(f"{int(counts.get('pending') or 0)}\n")
|
|
64
|
+
return 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cmd_summary() -> int:
|
|
68
|
+
counts = _counts_dict()
|
|
69
|
+
sent = int(counts.get("sent") or 0)
|
|
70
|
+
pending = int(counts.get("pending") or 0)
|
|
71
|
+
sys.stdout.write(f"{sent} {pending}\n")
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def cmd_outreach_queue() -> int:
|
|
76
|
+
resp = api_get(
|
|
77
|
+
"/api/v1/dms/outreach-queue",
|
|
78
|
+
query={
|
|
79
|
+
"platform": "twitter",
|
|
80
|
+
"status": "pending",
|
|
81
|
+
"limit": 200,
|
|
82
|
+
"other_engagement_days": 60,
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
rows = (resp.get("data") or {}).get("rows") or []
|
|
86
|
+
# The legacy psql query returned an array (json_agg result); reshape
|
|
87
|
+
# to that same array shape. Each row already carries the embedded
|
|
88
|
+
# other_engagement array from the route's correlated subquery.
|
|
89
|
+
sys.stdout.write(json.dumps(rows))
|
|
90
|
+
sys.stdout.write("\n")
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_recover_mcp(session_id: str) -> int:
|
|
95
|
+
resp = api_post(
|
|
96
|
+
"/api/v1/dms/recover-mcp-failures",
|
|
97
|
+
{"platform": "twitter", "claude_session_id": session_id},
|
|
98
|
+
)
|
|
99
|
+
d = resp.get("data") or {}
|
|
100
|
+
sys.stdout.write(f"{int(d.get('recovered_count') or 0)}\n")
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main() -> int:
|
|
105
|
+
ap = argparse.ArgumentParser(description="Helper for dm-outreach-twitter.sh")
|
|
106
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
107
|
+
|
|
108
|
+
sub.add_parser("pending-count")
|
|
109
|
+
sub.add_parser("outreach-queue")
|
|
110
|
+
sub.add_parser("summary")
|
|
111
|
+
|
|
112
|
+
p_rec = sub.add_parser("recover-mcp")
|
|
113
|
+
p_rec.add_argument("--session-id", required=True)
|
|
114
|
+
|
|
115
|
+
args = ap.parse_args()
|
|
116
|
+
|
|
117
|
+
if args.cmd == "pending-count":
|
|
118
|
+
return cmd_pending_count()
|
|
119
|
+
if args.cmd == "summary":
|
|
120
|
+
return cmd_summary()
|
|
121
|
+
if args.cmd == "outreach-queue":
|
|
122
|
+
return cmd_outreach_queue()
|
|
123
|
+
if args.cmd == "recover-mcp":
|
|
124
|
+
return cmd_recover_mcp(args.session_id)
|
|
125
|
+
return 1
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
sys.exit(main())
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Log a successful, verified DM send.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
dm_send_log.py --dm-id DM_ID --message TEXT --verified \
|
|
6
|
+
[--session-id UUID]
|
|
7
|
+
|
|
8
|
+
REQUIRES --verified. Without it the script refuses to flip status='sent'.
|
|
9
|
+
This is the gate against the prompt-driven "always mark sent" bug that
|
|
10
|
+
produced ~700 phantom rows in April 2026. The browser tool's send_dm /
|
|
11
|
+
compose_dm now returns ok=False when DOM verification fails; the LLM
|
|
12
|
+
running the outreach pipeline must only call this script when the tool
|
|
13
|
+
actually returned verified=true.
|
|
14
|
+
"""
|
|
15
|
+
import argparse
|
|
16
|
+
import os
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_env():
|
|
24
|
+
env_path = "/Users/matthewdi/social-autoposter/.env"
|
|
25
|
+
if not os.path.exists(env_path):
|
|
26
|
+
return
|
|
27
|
+
for line in open(env_path):
|
|
28
|
+
line = line.strip()
|
|
29
|
+
if not line or line.startswith("#"):
|
|
30
|
+
continue
|
|
31
|
+
if "=" in line:
|
|
32
|
+
k, v = line.split("=", 1)
|
|
33
|
+
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main():
|
|
37
|
+
parser = argparse.ArgumentParser(
|
|
38
|
+
description="Log a verified DM send (gates status='sent' on --verified)."
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument("--dm-id", required=True, help="dms.id")
|
|
41
|
+
parser.add_argument("--message", required=True, help="DM body that was sent")
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--verified",
|
|
44
|
+
action="store_true",
|
|
45
|
+
help="REQUIRED. Confirms the browser tool returned verified=true.",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--session-id",
|
|
49
|
+
default=os.environ.get("CLAUDE_SESSION_ID"),
|
|
50
|
+
help="claude_session_id UUID (defaults to $CLAUDE_SESSION_ID)",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Back-compat: old call sites used positional dm_id + message.
|
|
54
|
+
# Detect that shape so we can refuse cleanly instead of crashing.
|
|
55
|
+
if len(sys.argv) >= 3 and not sys.argv[1].startswith("--"):
|
|
56
|
+
print(
|
|
57
|
+
"ERROR: dm_send_log.py now requires named flags. Call as:\n"
|
|
58
|
+
" dm_send_log.py --dm-id ID --message TEXT --verified",
|
|
59
|
+
file=sys.stderr,
|
|
60
|
+
)
|
|
61
|
+
sys.exit(2)
|
|
62
|
+
|
|
63
|
+
args = parser.parse_args()
|
|
64
|
+
|
|
65
|
+
if not args.verified:
|
|
66
|
+
print(
|
|
67
|
+
"ERROR: refusing to mark dm_id={} as sent without --verified.\n"
|
|
68
|
+
"The browser send_dm/compose_dm tool must return verified=true "
|
|
69
|
+
"first. If verification failed, mark the row as 'error' instead.".format(
|
|
70
|
+
args.dm_id
|
|
71
|
+
),
|
|
72
|
+
file=sys.stderr,
|
|
73
|
+
)
|
|
74
|
+
sys.exit(3)
|
|
75
|
+
|
|
76
|
+
load_env()
|
|
77
|
+
import http_api
|
|
78
|
+
from version import read_version as read_autoposter_version
|
|
79
|
+
patch_body: dict = {"status": "sent", "our_dm_content": args.message}
|
|
80
|
+
if args.session_id:
|
|
81
|
+
patch_body["claude_session_id"] = args.session_id
|
|
82
|
+
# autoposter_version: stamp on the 'sent' transition so DM engagement
|
|
83
|
+
# (replies / bookings) can be attributed to the release of the autoposter
|
|
84
|
+
# code that drafted the message.
|
|
85
|
+
autoposter_version = read_autoposter_version()
|
|
86
|
+
if autoposter_version:
|
|
87
|
+
patch_body["autoposter_version"] = autoposter_version
|
|
88
|
+
http_api.api_patch(f"/api/v1/dms/{args.dm_id}", patch_body)
|
|
89
|
+
|
|
90
|
+
subprocess.run(
|
|
91
|
+
[
|
|
92
|
+
"python3",
|
|
93
|
+
"/Users/matthewdi/social-autoposter/scripts/dm_conversation.py",
|
|
94
|
+
"log-outbound",
|
|
95
|
+
"--dm-id",
|
|
96
|
+
str(args.dm_id),
|
|
97
|
+
"--content",
|
|
98
|
+
args.message,
|
|
99
|
+
"--verified",
|
|
100
|
+
],
|
|
101
|
+
check=True,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
main()
|