@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,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())
|