@m13v/s4l 1.6.197-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/README.md +143 -0
  2. package/SKILL.md +342 -0
  3. package/bin/cli.js +980 -0
  4. package/bin/cookie-helper.js +315 -0
  5. package/bin/platform.js +59 -0
  6. package/bin/scheduler/index.js +12 -0
  7. package/bin/scheduler/launchd.js +518 -0
  8. package/browser-agent-configs/all-agents-mcp.json +68 -0
  9. package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
  10. package/browser-agent-configs/linkedin-agent.json +17 -0
  11. package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
  12. package/browser-agent-configs/reddit-agent-mcp.json +16 -0
  13. package/browser-agent-configs/reddit-agent.json +17 -0
  14. package/browser-agent-configs/twitter-harness-mcp.json +18 -0
  15. package/config.example.json +45 -0
  16. package/mcp/dist/index.js +4212 -0
  17. package/mcp/dist/onboarding.js +200 -0
  18. package/mcp/dist/panel.html +176 -0
  19. package/mcp/dist/product-link.html +102 -0
  20. package/mcp/dist/repo.js +222 -0
  21. package/mcp/dist/runtime.js +1079 -0
  22. package/mcp/dist/screencast.js +323 -0
  23. package/mcp/dist/setup.js +545 -0
  24. package/mcp/dist/telemetry.js +306 -0
  25. package/mcp/dist/twitterAuth.js +138 -0
  26. package/mcp/dist/version.js +271 -0
  27. package/mcp/dist/version.json +4 -0
  28. package/mcp/install-runtime.mjs +70 -0
  29. package/mcp/install.mjs +169 -0
  30. package/mcp/manifest.json +80 -0
  31. package/mcp/menubar/dashboard_server.py +213 -0
  32. package/mcp/menubar/s4l_card.py +1314 -0
  33. package/mcp/menubar/s4l_log_relay.py +179 -0
  34. package/mcp/menubar/s4l_menubar.py +2439 -0
  35. package/mcp/menubar/s4l_state.py +891 -0
  36. package/mcp/package.json +34 -0
  37. package/mcp/shared/doctor.cjs +437 -0
  38. package/mcp/shared/onboarding-ledger.cjs +324 -0
  39. package/mcp-servers/browser-harness/server.py +968 -0
  40. package/package.json +160 -0
  41. package/requirements.txt +20 -0
  42. package/scripts/_compute_allowlist.py +58 -0
  43. package/scripts/_db_update.py +20 -0
  44. package/scripts/_filt.py +9 -0
  45. package/scripts/_li_notif_match.py +76 -0
  46. package/scripts/_li_notif_orchestrate.py +126 -0
  47. package/scripts/_lock_preempt_test.py +60 -0
  48. package/scripts/_run_icp_precheck.py +57 -0
  49. package/scripts/a16z_pearx_calendar_reminders.py +99 -0
  50. package/scripts/account_resolver.py +141 -0
  51. package/scripts/active_campaigns.py +114 -0
  52. package/scripts/active_users.py +190 -0
  53. package/scripts/amplitude_24h_signups.py +468 -0
  54. package/scripts/amplitude_signups.py +177 -0
  55. package/scripts/apply_onboarding_selections.py +131 -0
  56. package/scripts/audience_pages.py +243 -0
  57. package/scripts/audit_helper.py +120 -0
  58. package/scripts/author_history_block.py +353 -0
  59. package/scripts/autopilot_stall_watch.py +284 -0
  60. package/scripts/backfill_twitter_attempts_topic.py +81 -0
  61. package/scripts/backfill_twitter_log_post_no_id.py +322 -0
  62. package/scripts/bench_dashboard.sh +138 -0
  63. package/scripts/bh_send.py +39 -0
  64. package/scripts/build_persona.py +409 -0
  65. package/scripts/bulk_icp.py +18 -0
  66. package/scripts/campaign_bump.py +51 -0
  67. package/scripts/capture_thread_media.py +288 -0
  68. package/scripts/check_browser_lock_health.sh +81 -0
  69. package/scripts/check_external_pool_depth.py +253 -0
  70. package/scripts/check_unread_web_chats.py +28 -0
  71. package/scripts/claim_web_chat.py +47 -0
  72. package/scripts/classify_run_error.py +158 -0
  73. package/scripts/claude_job.py +988 -0
  74. package/scripts/clean_stale_singleton.sh +56 -0
  75. package/scripts/cleanup_harness_tabs.py +68 -0
  76. package/scripts/copy_browser_cookies.py +454 -0
  77. package/scripts/counterparty_history.py +350 -0
  78. package/scripts/db.py +57 -0
  79. package/scripts/discover_claude_profiles.py +120 -0
  80. package/scripts/discover_linkedin_candidates.py +984 -0
  81. package/scripts/dm_conversation.py +682 -0
  82. package/scripts/dm_db_update.py +69 -0
  83. package/scripts/dm_engage_helper.py +161 -0
  84. package/scripts/dm_outreach_helper.py +147 -0
  85. package/scripts/dm_outreach_twitter_helper.py +129 -0
  86. package/scripts/dm_send_log.py +106 -0
  87. package/scripts/dm_short_links.py +1084 -0
  88. package/scripts/dump_web_chat_history.py +47 -0
  89. package/scripts/engage_github.py +640 -0
  90. package/scripts/engage_reddit.py +1235 -0
  91. package/scripts/engage_twitter_helper.py +301 -0
  92. package/scripts/engagement_styles.py +1787 -0
  93. package/scripts/enrich_twitter_candidates.py +82 -0
  94. package/scripts/feedback_digest.py +448 -0
  95. package/scripts/fetch_prospect_profile.py +312 -0
  96. package/scripts/fetch_twitter_t1.py +134 -0
  97. package/scripts/find_threads.py +530 -0
  98. package/scripts/follow_gate_log.py +59 -0
  99. package/scripts/funnel_per_day.py +194 -0
  100. package/scripts/generate_daily_human_style.py +494 -0
  101. package/scripts/generation_trace.py +173 -0
  102. package/scripts/get_run_cost.py +107 -0
  103. package/scripts/github_engage_helper.py +93 -0
  104. package/scripts/github_tools.py +509 -0
  105. package/scripts/harness_overlay.py +556 -0
  106. package/scripts/harvest_twitter_following.py +243 -0
  107. package/scripts/heartbeat.sh +70 -0
  108. package/scripts/history_context.py +284 -0
  109. package/scripts/http_api.py +206 -0
  110. package/scripts/human_dm_replies_helper.py +169 -0
  111. package/scripts/identity.py +302 -0
  112. package/scripts/ig_batch_creator.sh +93 -0
  113. package/scripts/ig_post_type_picker.py +243 -0
  114. package/scripts/ig_scrape_transcribe.sh +91 -0
  115. package/scripts/ingest_human_dm_replies.py +271 -0
  116. package/scripts/ingest_web_chat_replies.py +229 -0
  117. package/scripts/install_fleet.py +187 -0
  118. package/scripts/invent_mcp_server.py +350 -0
  119. package/scripts/invent_topics.py +1462 -0
  120. package/scripts/learned_preferences.py +263 -0
  121. package/scripts/li_discovery.py +161 -0
  122. package/scripts/link_edit_helper.py +142 -0
  123. package/scripts/link_tail.py +592 -0
  124. package/scripts/linkedin_api.py +561 -0
  125. package/scripts/linkedin_browser.py +730 -0
  126. package/scripts/linkedin_cooldown.py +128 -0
  127. package/scripts/linkedin_exclusions.py +234 -0
  128. package/scripts/linkedin_killswitch.py +1333 -0
  129. package/scripts/linkedin_search_topic_schema.py +49 -0
  130. package/scripts/linkedin_unipile.py +658 -0
  131. package/scripts/linkedin_url.py +228 -0
  132. package/scripts/log_claude_session.py +636 -0
  133. package/scripts/log_draft.py +143 -0
  134. package/scripts/log_linkedin_search_attempts.py +126 -0
  135. package/scripts/log_post.py +651 -0
  136. package/scripts/log_run.py +364 -0
  137. package/scripts/log_thread_media.py +108 -0
  138. package/scripts/log_twitter_search_attempts.py +150 -0
  139. package/scripts/log_twitter_skips.py +211 -0
  140. package/scripts/lookup_post.py +78 -0
  141. package/scripts/mark_web_chat_processed.py +32 -0
  142. package/scripts/mcp_lock_proxy.py +370 -0
  143. package/scripts/memory_snapshot.py +972 -0
  144. package/scripts/merge_review_queue.py +215 -0
  145. package/scripts/mint_external_pool.py +182 -0
  146. package/scripts/mint_kent_pool.py +249 -0
  147. package/scripts/moltbook_post.py +320 -0
  148. package/scripts/moltbook_tools.py +159 -0
  149. package/scripts/pending_threads.py +188 -0
  150. package/scripts/pick_ig_account.py +177 -0
  151. package/scripts/pick_project.py +208 -0
  152. package/scripts/pick_search_topic.py +771 -0
  153. package/scripts/pick_thread_target.py +279 -0
  154. package/scripts/pick_twitter_thread_target.py +202 -0
  155. package/scripts/podlog_fetch_batch.sh +32 -0
  156. package/scripts/post_github.py +1311 -0
  157. package/scripts/post_reddit.py +2668 -0
  158. package/scripts/precompute_dashboard_stats.py +204 -0
  159. package/scripts/preflight.sh +297 -0
  160. package/scripts/progress.py +88 -0
  161. package/scripts/project_excludes.py +353 -0
  162. package/scripts/project_slugs.py +91 -0
  163. package/scripts/project_stats.py +241 -0
  164. package/scripts/project_stats_json.py +1563 -0
  165. package/scripts/project_topics.py +192 -0
  166. package/scripts/qualified_query_bank.py +436 -0
  167. package/scripts/reap_stale_claude_sessions.py +867 -0
  168. package/scripts/reddit_browser.py +2549 -0
  169. package/scripts/reddit_browser_fetch.py +141 -0
  170. package/scripts/reddit_browser_lock.py +593 -0
  171. package/scripts/reddit_chat_sync.py +710 -0
  172. package/scripts/reddit_query_bank.py +200 -0
  173. package/scripts/reddit_threads_helper.py +151 -0
  174. package/scripts/reddit_tools.py +956 -0
  175. package/scripts/refresh_instagram_tokens.py +280 -0
  176. package/scripts/release-mcpb.sh +497 -0
  177. package/scripts/reply_db.py +334 -0
  178. package/scripts/reply_insert.py +98 -0
  179. package/scripts/reply_risk_digest.py +761 -0
  180. package/scripts/reset-test-machine.sh +602 -0
  181. package/scripts/restore_twitter_session.py +177 -0
  182. package/scripts/ripen_reddit_plan.py +478 -0
  183. package/scripts/run_claude.sh +433 -0
  184. package/scripts/run_moltbook_cycle.py +555 -0
  185. package/scripts/s4l_box_update.sh +226 -0
  186. package/scripts/s4l_channel.py +103 -0
  187. package/scripts/s4l_ctl.sh +75 -0
  188. package/scripts/s4l_env.py +47 -0
  189. package/scripts/saps_activity.py +126 -0
  190. package/scripts/saps_mode.py +328 -0
  191. package/scripts/scan_dm_candidates.py +580 -0
  192. package/scripts/scan_github_replies.py +168 -0
  193. package/scripts/scan_instagram_comments.py +481 -0
  194. package/scripts/scan_moltbook_replies.py +252 -0
  195. package/scripts/scan_pii.py +190 -0
  196. package/scripts/scan_reddit_replies.py +377 -0
  197. package/scripts/scan_twitter_mentions_browser.py +327 -0
  198. package/scripts/scan_twitter_thread_followups.py +299 -0
  199. package/scripts/scan_x_profile.py +384 -0
  200. package/scripts/schedule_state.py +202 -0
  201. package/scripts/scheduled_tasks_snapshot.py +123 -0
  202. package/scripts/score_linkedin_candidates.py +419 -0
  203. package/scripts/score_twitter_candidates.py +718 -0
  204. package/scripts/scrape_linkedin_comment_stats.py +1755 -0
  205. package/scripts/scrape_linkedin_stats_browser.py +52 -0
  206. package/scripts/scrape_reddit_views.py +365 -0
  207. package/scripts/seed_search_queries.py +453 -0
  208. package/scripts/seed_search_topics.py +127 -0
  209. package/scripts/send_web_chat_reply.py +130 -0
  210. package/scripts/sentry_init.py +128 -0
  211. package/scripts/setup_twitter_auth.py +1320 -0
  212. package/scripts/snapshot.py +583 -0
  213. package/scripts/stats.py +2702 -0
  214. package/scripts/stats_helper.py +52 -0
  215. package/scripts/strike_alert.py +783 -0
  216. package/scripts/sweep_post_link_clicks.py +107 -0
  217. package/scripts/sync_ig_to_posts.py +147 -0
  218. package/scripts/test_browser_lock.py +189 -0
  219. package/scripts/test_installation_api.sh +52 -0
  220. package/scripts/test_percard_posting.py +142 -0
  221. package/scripts/top_dud_linkedin_queries.py +71 -0
  222. package/scripts/top_dud_reddit_queries.py +67 -0
  223. package/scripts/top_dud_twitter_queries.py +71 -0
  224. package/scripts/top_dud_twitter_topics.py +102 -0
  225. package/scripts/top_linkedin_queries.py +55 -0
  226. package/scripts/top_omitted_reddit_topics.py +91 -0
  227. package/scripts/top_performers.py +588 -0
  228. package/scripts/top_search_topics.py +180 -0
  229. package/scripts/top_twitter_queries.py +190 -0
  230. package/scripts/twitter_access_check.py +382 -0
  231. package/scripts/twitter_account.py +41 -0
  232. package/scripts/twitter_batch_phase.py +126 -0
  233. package/scripts/twitter_browser.py +2804 -0
  234. package/scripts/twitter_cookie_mirror.py +130 -0
  235. package/scripts/twitter_cycle_helper.py +310 -0
  236. package/scripts/twitter_gen_links.py +287 -0
  237. package/scripts/twitter_post_plan.py +1188 -0
  238. package/scripts/twitter_scan.py +324 -0
  239. package/scripts/twitter_supply_signal.py +57 -0
  240. package/scripts/twitter_threads_helper.py +152 -0
  241. package/scripts/unclaim_web_chat.py +29 -0
  242. package/scripts/update_instagram_stats.py +261 -0
  243. package/scripts/update_linkedin_stats_from_feed.py +328 -0
  244. package/scripts/version.py +72 -0
  245. package/scripts/watchdog_hung_runs.py +343 -0
  246. package/scripts/write_generation_trace.py +73 -0
  247. package/setup/SKILL.md +277 -0
  248. package/skill/amplitude-24h-signups.sh +38 -0
  249. package/skill/archive-old-logs.sh +40 -0
  250. package/skill/audit-dm-staleness.sh +42 -0
  251. package/skill/audit-linkedin.sh +14 -0
  252. package/skill/audit-moltbook.sh +4 -0
  253. package/skill/audit-reddit-resurrect.sh +67 -0
  254. package/skill/audit-reddit.sh +4 -0
  255. package/skill/audit-twitter.sh +4 -0
  256. package/skill/audit.sh +287 -0
  257. package/skill/backfill-twitter-attempts-topic.sh +19 -0
  258. package/skill/backfill-twitter-ghost-posts.sh +24 -0
  259. package/skill/check-external-pool-depth.sh +7 -0
  260. package/skill/check-web-chats.sh +203 -0
  261. package/skill/dm-outreach-linkedin.sh +250 -0
  262. package/skill/dm-outreach-reddit.sh +274 -0
  263. package/skill/dm-outreach-twitter.sh +265 -0
  264. package/skill/engage-dm-replies-linkedin.sh +4 -0
  265. package/skill/engage-dm-replies-reddit.sh +4 -0
  266. package/skill/engage-dm-replies-twitter.sh +4 -0
  267. package/skill/engage-dm-replies.sh +1597 -0
  268. package/skill/engage-linkedin.sh +581 -0
  269. package/skill/engage-moltbook.sh +36 -0
  270. package/skill/engage-reddit.sh +146 -0
  271. package/skill/engage-twitter.sh +467 -0
  272. package/skill/github-engage.sh +176 -0
  273. package/skill/ingest-web-chat-replies.sh +38 -0
  274. package/skill/invent-supply-test.sh +100 -0
  275. package/skill/invent-topics.sh +50 -0
  276. package/skill/lib/linkedin-backend.sh +364 -0
  277. package/skill/lib/platform.sh +48 -0
  278. package/skill/lib/reddit-backend.sh +234 -0
  279. package/skill/lib/twitter-backend.sh +314 -0
  280. package/skill/link-edit-github.sh +136 -0
  281. package/skill/link-edit-moltbook.sh +117 -0
  282. package/skill/link-edit-reddit.sh +201 -0
  283. package/skill/linkedin-presence.sh +182 -0
  284. package/skill/linkedin-recovery.sh +282 -0
  285. package/skill/lock.sh +647 -0
  286. package/skill/memory-snapshot.sh +39 -0
  287. package/skill/precompute-stats.sh +35 -0
  288. package/skill/prewarm-funnel.sh +104 -0
  289. package/skill/refresh-instagram-tokens.sh +57 -0
  290. package/skill/refresh-twitter-following.sh +52 -0
  291. package/skill/reply-risk-digest.sh +31 -0
  292. package/skill/run-cycle-update-guard.sh +44 -0
  293. package/skill/run-draft-and-publish.sh +123 -0
  294. package/skill/run-generate-daily-style.sh +50 -0
  295. package/skill/run-github-launchd.sh +62 -0
  296. package/skill/run-github.sh +102 -0
  297. package/skill/run-instagram-daily.sh +149 -0
  298. package/skill/run-instagram-render.sh +875 -0
  299. package/skill/run-linkedin-launchd.sh +81 -0
  300. package/skill/run-linkedin-unipile.sh +130 -0
  301. package/skill/run-linkedin.sh +1593 -0
  302. package/skill/run-moltbook-launchd.sh +61 -0
  303. package/skill/run-moltbook.sh +38 -0
  304. package/skill/run-overlay-watch.sh +100 -0
  305. package/skill/run-reddit-search-launchd.sh +64 -0
  306. package/skill/run-reddit-search.sh +505 -0
  307. package/skill/run-reddit-threads-double.sh +32 -0
  308. package/skill/run-reddit-threads.sh +847 -0
  309. package/skill/run-scan-moltbook-replies.sh +57 -0
  310. package/skill/run-twitter-cycle-launchd.sh +63 -0
  311. package/skill/run-twitter-cycle-singleton.sh +62 -0
  312. package/skill/run-twitter-cycle.sh +2408 -0
  313. package/skill/run-twitter-threads.sh +592 -0
  314. package/skill/scan-instagram-replies.sh +61 -0
  315. package/skill/scan-twitter-followups.sh +57 -0
  316. package/skill/social-autoposter-update.sh +66 -0
  317. package/skill/stats-instagram.sh +72 -0
  318. package/skill/stats-linkedin.sh +271 -0
  319. package/skill/stats-moltbook.sh +4 -0
  320. package/skill/stats-reddit.sh +4 -0
  321. package/skill/stats-twitter.sh +4 -0
  322. package/skill/stats.sh +521 -0
  323. package/skill/strike-alert.sh +18 -0
  324. package/skill/styles.sh +87 -0
  325. package/skill/sweep-link-clicks.sh +40 -0
  326. package/skill/topics.sh +51 -0
@@ -0,0 +1,173 @@
1
+ """Shared helpers for writing the generation_trace audit blob.
2
+
3
+ Every post-drafting pipeline (post_github.py, post_reddit.py,
4
+ run-twitter-cycle.sh + twitter_post_plan.py) builds a small JSON snapshot
5
+ of "what Claude saw" — top_performers report, top_search_topics report,
6
+ recent-comments cluster, prompt size, model, scoring formula — and
7
+ persists it to posts.generation_trace JSONB. This module owns the shape
8
+ and the file-handoff dance so the three pipelines stay consistent.
9
+
10
+ Why a shared module instead of duplicating the dict in each pipeline:
11
+ the trace shape is a contract with the audit consumer (a future "show
12
+ me which examples produced post #123" query). Drift between pipelines
13
+ makes that query impossible. Centralizing here means a `version`
14
+ bump migrates every writer at once.
15
+
16
+ Shape v1 — must match the comment block at the top of
17
+ migrations/2026-05-12_generation_trace.sql. Bump `TRACE_SHAPE_VERSION`
18
+ and add a migration if the contract changes.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import os
24
+ import tempfile
25
+ import uuid
26
+ from datetime import datetime
27
+ from typing import Iterable, Optional
28
+
29
+ TRACE_SHAPE_VERSION = 1
30
+
31
+ # API cap (matches src/app/api/v1/posts/route.ts). We do NOT truncate
32
+ # the report bodies — top_performers.py and top_search_topics.py
33
+ # already pre-summarize their output and rarely cross 10 KB combined.
34
+ # Cap raised from 64 KB to 1 MB on 2026-05-13: real Twitter cycles were
35
+ # producing ~85 KB traces and the API was rejecting every log_post call
36
+ # with HTTP 400 (posted=0 / failed=log_post_no_id on the dashboard while
37
+ # replies still landed on x.com). The "log_post just drops the trace
38
+ # field and warns" fallback in the older comment was aspirational, not
39
+ # implemented; bumping the cap is the correct lever here.
40
+ MAX_TRACE_BYTES = 1024 * 1024
41
+
42
+
43
+ def build_trace(
44
+ *,
45
+ platform: str,
46
+ project_name: str,
47
+ prompt_chars: int,
48
+ top_performers_text: str = "",
49
+ top_search_topics_text: str = "",
50
+ recent_comment_ids: Optional[Iterable[int]] = None,
51
+ model: Optional[str] = None,
52
+ min_score_floor: Optional[int] = None,
53
+ extras: Optional[dict] = None,
54
+ ) -> dict:
55
+ """Construct the canonical trace dict for one Claude drafting run.
56
+
57
+ All examples-strings are stored verbatim. We deliberately do NOT
58
+ re-derive structure (e.g. "parse top_performers_text and pull out
59
+ each example as a sub-object") — the bytes-for-bytes report is the
60
+ only audit-faithful representation of what landed in the prompt.
61
+
62
+ `extras` is a per-pipeline escape hatch (twitter passes top_queries
63
+ + supply_signal; reddit passes dud_queries). Stored under
64
+ `examples.extras` so the schema stays stable.
65
+ """
66
+ return {
67
+ "version": TRACE_SHAPE_VERSION,
68
+ "generated_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
69
+ "model": model or "",
70
+ "platform": platform,
71
+ "project": project_name,
72
+ "prompt_chars": int(prompt_chars or 0),
73
+ "examples": {
74
+ "top_performers_text": top_performers_text or "",
75
+ "top_search_topics_text": top_search_topics_text or "",
76
+ "recent_comment_ids": [int(x) for x in (recent_comment_ids or [])],
77
+ "extras": dict(extras or {}),
78
+ },
79
+ "scoring": {
80
+ "score_formula": "clicks*10 + comments*3 + upvotes_net",
81
+ "min_score_floor": int(min_score_floor or 0),
82
+ "click_aware_since": "2026-05-12",
83
+ },
84
+ }
85
+
86
+
87
+ _GEN_TRACE_DIR_ENV = "SOCIAL_AUTOPOSTER_GEN_TRACE_DIR"
88
+
89
+
90
+ def _gen_trace_dir() -> Optional[str]:
91
+ """Resolve the persistent trace directory.
92
+
93
+ Order: env override > <repo>/log/gen_trace/ > None (fall back to tempfile).
94
+ """
95
+ override = os.environ.get(_GEN_TRACE_DIR_ENV)
96
+ if override:
97
+ try:
98
+ os.makedirs(override, exist_ok=True)
99
+ return override
100
+ except OSError:
101
+ return None
102
+ repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
103
+ target = os.path.join(repo_dir, "log", "gen_trace")
104
+ try:
105
+ os.makedirs(target, exist_ok=True)
106
+ return target
107
+ except OSError:
108
+ return None
109
+
110
+
111
+ def write_trace_tempfile(trace: dict, *, prefix: str = "gen_trace_") -> Optional[str]:
112
+ """Persist trace dict to disk and return the path.
113
+
114
+ Was a /tmp NamedTemporaryFile until 2026-05-14; /tmp didn't survive
115
+ e2b sandbox pause/resume, losing per-thread draft decisions on the
116
+ very runs we needed to postmortem. Now writes to <repo>/log/gen_trace/
117
+ (or $SOCIAL_AUTOPOSTER_GEN_TRACE_DIR), persistent across the run.
118
+
119
+ Returns None on any IO failure — the caller must treat the trace as
120
+ nice-to-have, never block the post on a failed trace write.
121
+ """
122
+ trace_dir = _gen_trace_dir()
123
+ try:
124
+ if trace_dir:
125
+ ts = datetime.utcnow().strftime("%Y%m%d-%H%M%S")
126
+ fname = f"{prefix}{ts}_{uuid.uuid4().hex[:8]}.json"
127
+ path = os.path.join(trace_dir, fname)
128
+ with open(path, "w", encoding="utf-8") as fh:
129
+ json.dump(trace, fh, ensure_ascii=False)
130
+ return path
131
+ # Fallback: tempfile (read-only repo, sandbox edge cases).
132
+ tf = tempfile.NamedTemporaryFile(
133
+ prefix=prefix, suffix=".json",
134
+ mode="w", delete=False, encoding="utf-8",
135
+ )
136
+ try:
137
+ json.dump(trace, tf, ensure_ascii=False)
138
+ finally:
139
+ tf.close()
140
+ return tf.name
141
+ except (OSError, TypeError, ValueError):
142
+ return None
143
+
144
+
145
+ def cleanup_trace_tempfile(path: Optional[str]) -> None:
146
+ """Historically deleted trace files at end-of-run. Now a no-op for files
147
+ under <repo>/log/ so failure postmortems retain trace JSON; still deletes
148
+ legacy /tmp paths to avoid filling /tmp on long-running hosts.
149
+ """
150
+ if not path:
151
+ return
152
+ try:
153
+ # Persistent log/ files: keep. Anything else (e.g. /tmp fallback): delete.
154
+ norm = os.path.abspath(path)
155
+ repo_log = os.path.abspath(os.path.join(
156
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
157
+ "log",
158
+ ))
159
+ override = os.environ.get(_GEN_TRACE_DIR_ENV)
160
+ keep_dirs = [repo_log] + ([os.path.abspath(override)] if override else [])
161
+ if any(norm.startswith(d + os.sep) for d in keep_dirs):
162
+ return
163
+ os.unlink(path)
164
+ except OSError:
165
+ pass
166
+
167
+
168
+ def trace_bytes(trace: dict) -> int:
169
+ """Serialized size in bytes. Useful for guard checks before write."""
170
+ try:
171
+ return len(json.dumps(trace, ensure_ascii=False).encode("utf-8"))
172
+ except (TypeError, ValueError):
173
+ return 0
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ """Query claude_sessions to get total cost for a pipeline cycle.
3
+
4
+ Two modes:
5
+
6
+ 1) Cycle mode (preferred): `--cycle-id rdcycle-...`
7
+ Sums orchestrator_cost_usd (native SDK billing) for every row stamped with
8
+ this cycle_id. This is the authoritative Anthropic bill, NOT the inflated
9
+ transcript-derived estimate (total_cost_usd) we used to report. Accurate
10
+ even when multiple cycles of the same script overlap in wall-clock time
11
+ (run-reddit-search.sh, run-twitter-cycle.sh double-fork their work so
12
+ stacked cycles are normal).
13
+
14
+ 2) Legacy time-window mode: `--since <unix_ts> --scripts tag1 tag2 ...`
15
+ Filters by script + started_at. Kept for backward compatibility with
16
+ callers that don't pass cycle_id (older pipelines, historical reports).
17
+ IMPORTANT: this mode over-counts when multiple cycles of the same script
18
+ overlap, because it has no way to distinguish them. Migrate callers to
19
+ --cycle-id when possible.
20
+
21
+ Either mode is acceptable; --cycle-id wins if both are passed. Prints the
22
+ total cost as a float (4 decimal places), or 0.0000 on any error.
23
+ Designed to be called from shell script EXIT traps to get real cost per run.
24
+ """
25
+ import argparse
26
+ import os
27
+ import sys
28
+ from datetime import datetime, timezone
29
+
30
+ ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
31
+
32
+
33
+ def _load_env():
34
+ env_path = os.path.join(ROOT_DIR, '.env')
35
+ if not os.path.exists(env_path):
36
+ return
37
+ with open(env_path) as f:
38
+ for line in f:
39
+ line = line.strip()
40
+ if line and not line.startswith('#') and '=' in line:
41
+ k, v = line.split('=', 1)
42
+ os.environ.setdefault(k.strip(), v.strip())
43
+
44
+
45
+ def main():
46
+ p = argparse.ArgumentParser()
47
+ p.add_argument('--cycle-id', default=None,
48
+ help='Pipeline cycle batch id (e.g. rdcycle-20260510-110005). '
49
+ 'Sums cost across every claude_sessions row stamped with '
50
+ 'this id. Wins over --since/--scripts when both passed.')
51
+ p.add_argument('--since', type=int, default=None,
52
+ help='Unix timestamp of run start (legacy mode; use cycle-id '
53
+ 'instead when possible).')
54
+ p.add_argument('--scripts', nargs='*', default=None,
55
+ help='claude_sessions.script values to sum (legacy mode).')
56
+ p.add_argument('--breakdown', action='store_true',
57
+ help='Print "parent_cost subagent_cost task_count subagent_count" '
58
+ 'instead of just the parent total. Useful when investigating '
59
+ 'whether Task() subagents are inflating cost.')
60
+ args = p.parse_args()
61
+
62
+ _load_env()
63
+
64
+ # Resolve which mode we're running in. Cycle id is authoritative if
65
+ # given (and non-empty). Otherwise require the legacy pair.
66
+ cycle_id = args.cycle_id.strip() if args.cycle_id else None
67
+ if not cycle_id and (args.since is None or not args.scripts):
68
+ # Bash EXIT traps shell out blind; keep the contract simple: any
69
+ # missing-arg condition prints 0.0000 and exits 0 so the caller
70
+ # never crashes its log_run.py emit on a malformed cost call.
71
+ print("0.0000")
72
+ return
73
+
74
+ try:
75
+ sys.path.insert(0, os.path.join(ROOT_DIR, 'scripts'))
76
+ # HTTP-only: cost is always read from the s4l.ai HTTP API. The direct
77
+ # Postgres path (and the SOCIAL_AUTOPOSTER_LEGACY_NEON escape hatch) was
78
+ # removed 2026-06-01 — no DB path, no fallback.
79
+ parent_cost, subagent_cost, task_count, subagent_count = _fetch_via_api(
80
+ cycle_id=cycle_id, since=args.since, scripts=args.scripts,
81
+ )
82
+ if args.breakdown:
83
+ print(f"{parent_cost:.4f} {subagent_cost:.4f} {task_count} {subagent_count}")
84
+ else:
85
+ print(f"{parent_cost:.4f}")
86
+ except Exception:
87
+ print("0.0000")
88
+
89
+
90
+ def _fetch_via_api(*, cycle_id, since, scripts):
91
+ from http_api import api_get
92
+ if cycle_id:
93
+ query = {"cycle_id": cycle_id}
94
+ else:
95
+ query = {"since_ts": str(int(since)), "scripts": ",".join(scripts)}
96
+ resp = api_get("/api/v1/claude-sessions/cost", query=query)
97
+ data = (resp or {}).get("data") or {}
98
+ return (
99
+ float(data.get("parent_cost") or 0),
100
+ float(data.get("subagent_cost") or 0),
101
+ int(data.get("task_count") or 0),
102
+ int(data.get("subagent_count") or 0),
103
+ )
104
+
105
+
106
+ if __name__ == '__main__':
107
+ main()
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env python3
2
+ """github_engage_helper.py — small CLI wrapper used by skill/github-engage.sh
3
+ to replace the five `psql "$DATABASE_URL" -t -A -c "..."` one-liners the shell
4
+ used to embed inline. The direct-Postgres lane was removed 2026-06-01;
5
+ DATABASE_URL is deliberately ignored, no DB, no fallback. Every subcommand
6
+ prints exactly what the corresponding psql call printed so the surrounding
7
+ shell capture ($(...)) and integer compares are unchanged.
8
+
9
+ Subcommands:
10
+ posts-active-count
11
+ -> GET /api/v1/posts/count?platform=github&status=active
12
+ -> prints the integer count (was: SELECT COUNT(*) FROM posts
13
+ WHERE platform='github' AND status='active')
14
+ pending-count
15
+ -> GET /api/v1/replies/counts?platform=github
16
+ -> prints the integer pending count (was: SELECT COUNT(*) FROM replies
17
+ WHERE platform='github' AND status='pending')
18
+ reply-counts
19
+ -> GET /api/v1/replies/counts?platform=github
20
+ -> prints JSON {pending, replied, skipped} (replaces the three trailing
21
+ psql COUNT one-liners in Phase C)
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import json
27
+ import os
28
+ import sys
29
+
30
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
31
+ from http_api import api_get # noqa: E402
32
+
33
+
34
+ def _counts_dict() -> dict[str, int]:
35
+ resp = api_get("/api/v1/replies/counts", query={"platform": "github"})
36
+ data = resp.get("data") or {}
37
+ # github has no mentions/orphan nuance; the raw `counts` field is the
38
+ # authoritative per-status tally. (eligible_counts would also work but
39
+ # for github every reply is post-rooted.)
40
+ rows = data.get("counts") or []
41
+ out: dict[str, int] = {}
42
+ for r in rows:
43
+ s = r.get("status")
44
+ if s is None:
45
+ continue
46
+ try:
47
+ out[str(s)] = int(r.get("count") or 0)
48
+ except (TypeError, ValueError):
49
+ out[str(s)] = 0
50
+ return out
51
+
52
+
53
+ def cmd_posts_active_count() -> int:
54
+ resp = api_get("/api/v1/posts/count", query={"platform": "github", "status": "active"})
55
+ print(int((resp.get("data") or {}).get("count") or 0))
56
+ return 0
57
+
58
+
59
+ def cmd_pending_count() -> int:
60
+ print(int(_counts_dict().get("pending") or 0))
61
+ return 0
62
+
63
+
64
+ def cmd_reply_counts() -> int:
65
+ counts = _counts_dict()
66
+ out = {
67
+ "pending": int(counts.get("pending") or 0),
68
+ "replied": int(counts.get("replied") or 0),
69
+ "skipped": int(counts.get("skipped") or 0),
70
+ }
71
+ json.dump(out, sys.stdout, separators=(",", ":"))
72
+ sys.stdout.write("\n")
73
+ return 0
74
+
75
+
76
+ def main() -> int:
77
+ p = argparse.ArgumentParser()
78
+ sub = p.add_subparsers(dest="cmd", required=True)
79
+ sub.add_parser("posts-active-count")
80
+ sub.add_parser("pending-count")
81
+ sub.add_parser("reply-counts")
82
+ args = p.parse_args()
83
+ if args.cmd == "posts-active-count":
84
+ return cmd_posts_active_count()
85
+ if args.cmd == "pending-count":
86
+ return cmd_pending_count()
87
+ if args.cmd == "reply-counts":
88
+ return cmd_reply_counts()
89
+ return 1
90
+
91
+
92
+ if __name__ == "__main__":
93
+ sys.exit(main())