@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,433 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Wrapper around `claude` that pre-assigns a session UUID, exports it for
|
|
3
|
+
# downstream loggers (log_post.py, reply_db.py, dm_conversation.py read
|
|
4
|
+
# CLAUDE_SESSION_ID from env), and after the session exits records token
|
|
5
|
+
# usage + computed cost into the claude_sessions table.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# scripts/run_claude.sh <script_tag> -p "PROMPT" [other claude flags...]
|
|
9
|
+
#
|
|
10
|
+
# Runner migration pattern:
|
|
11
|
+
# OLD: claude -p "PROMPT" 2>&1 | tee -a "$LOG_FILE"
|
|
12
|
+
# NEW: scripts/run_claude.sh "run-linkedin" -p "PROMPT" 2>&1 | tee -a "$LOG_FILE"
|
|
13
|
+
#
|
|
14
|
+
# The wrapper passes everything after the script_tag verbatim to `claude`,
|
|
15
|
+
# so all flags (--output-format, --json-schema, --model, etc.) work unchanged.
|
|
16
|
+
# stdout is streamed straight from claude — no buffering — so existing pipes
|
|
17
|
+
# and parsers see identical output.
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
# SAPS_->S4L_ env mirror (brand rename 2026-07-03): old plists/tasks still
|
|
22
|
+
# export SAPS_*; new code reads S4L_*. Copy names, never values via eval.
|
|
23
|
+
while IFS='=' read -r _k _; do
|
|
24
|
+
case "$_k" in SAPS_*) _n="S4L_${_k#SAPS_}"; eval "[ -n \"\${$_n+x}\" ] || export $_n=\"\${$_k}\"";; esac
|
|
25
|
+
done <<EOF_ENV
|
|
26
|
+
$(env | grep '^SAPS_' | cut -d= -f1 | sed 's/$/=/')
|
|
27
|
+
EOF_ENV
|
|
28
|
+
|
|
29
|
+
if [ $# -lt 2 ]; then
|
|
30
|
+
echo "Usage: run_claude.sh <script_tag> <claude args...>" >&2
|
|
31
|
+
exit 2
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
SCRIPT_TAG="$1"; shift
|
|
35
|
+
|
|
36
|
+
# If the caller pre-set CLAUDE_SESSION_ID, honor it. This lets calling
|
|
37
|
+
# scripts inject the same UUID into their prompt (e.g. for SQL inserts that
|
|
38
|
+
# need to stamp claude_session_id) before invoking the wrapper.
|
|
39
|
+
SESSION_ID="${CLAUDE_SESSION_ID:-$(uuidgen | tr 'A-Z' 'a-z')}"
|
|
40
|
+
export CLAUDE_SESSION_ID="$SESSION_ID"
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Queue provider seam (added 2026-06-23) — customer boxes have no `claude` CLI.
|
|
44
|
+
#
|
|
45
|
+
# On a .mcpb install the runtime provisions Python only; there is no `claude`
|
|
46
|
+
# binary to exec. When S4L_CLAUDE_PROVIDER=queue, route this call through the
|
|
47
|
+
# job queue instead: scripts/claude_job.py enqueues the prompt + --json-schema,
|
|
48
|
+
# BLOCKS until a Claude Desktop scheduled task ("saps-phase1-query" /
|
|
49
|
+
# "saps-phase2b-draft") processes it, and prints a claude `--output-format json`
|
|
50
|
+
# -shaped envelope to stdout so the pipeline's existing parsers are unchanged.
|
|
51
|
+
#
|
|
52
|
+
# Provider unset (your own machines, every launchd plist) => fall straight
|
|
53
|
+
# through to the real `claude -p` below, byte-for-byte unchanged. The seam lives
|
|
54
|
+
# here, in the single chokepoint every claude call funnels through, so NO
|
|
55
|
+
# pipeline script needs to know whether it's on a customer box or yours.
|
|
56
|
+
#
|
|
57
|
+
# The prompt reaches us either as a trailing positional arg (Phase 1 queries) or
|
|
58
|
+
# piped on stdin (Phase 2b prep); claude_job.py handles both. `exec` inherits
|
|
59
|
+
# stdin so the piped form passes through, and propagates the helper's exit code
|
|
60
|
+
# (0 = result, 79 = timed-out/blocked like a quota skip, 1 = not queue-eligible).
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
if [ "${S4L_CLAUDE_PROVIDER:-}" = "queue" ]; then
|
|
63
|
+
S4L_QUEUE_REPO="${S4L_REPO_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
64
|
+
S4L_QUEUE_PY="${S4L_PYTHON:-python3}"
|
|
65
|
+
exec "$S4L_QUEUE_PY" "$S4L_QUEUE_REPO/scripts/claude_job.py" \
|
|
66
|
+
provider --tag "$SCRIPT_TAG" -- "$@"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Quota preflight + post-hoc detection (added 2026-05-02).
|
|
71
|
+
#
|
|
72
|
+
# Background: if claude is hitting an org-level cap (monthly usage cap,
|
|
73
|
+
# daily token cap, context-window exceeded on every prompt, credit balance
|
|
74
|
+
# zero, persistent 429), retrying every cadence-tick burns nothing useful —
|
|
75
|
+
# it just guarantees an empty envelope back to the pipeline, which then
|
|
76
|
+
# fails noisily downstream (cf. 2026-05-01 19:23 twitter-cycle that died
|
|
77
|
+
# in Phase 1 on the org monthly limit and produced 0 tweets, then 2 more
|
|
78
|
+
# cycles fired into the same wall).
|
|
79
|
+
#
|
|
80
|
+
# Mechanism (see scripts/preflight.sh for full design):
|
|
81
|
+
# - At start: check /tmp/sa-claude-blocked.json. If `blocked_until > now`,
|
|
82
|
+
# exit 79 immediately. The wrapper exits visibly (stderr `[skipped]`
|
|
83
|
+
# line) and the calling pipeline can either (a) abort gracefully on
|
|
84
|
+
# non-zero, or (b) check `$? == 79` and treat as "skip cycle" rather
|
|
85
|
+
# than "real failure".
|
|
86
|
+
# - After claude exits: scan SIDE_LOG for known fatal-quota patterns. On
|
|
87
|
+
# match, write a fresh stamp (10 min default) and force exit 79.
|
|
88
|
+
# - On a clean claude run, if a stamp is present, clear it — the cap has
|
|
89
|
+
# lifted and we shouldn't gate the next cycle.
|
|
90
|
+
#
|
|
91
|
+
# Block window = 10 min. The next launchd fire after expiry will retry
|
|
92
|
+
# claude for real. If the underlying cap is still in place, we re-stamp
|
|
93
|
+
# and skip again. This recovers automatically within 10 min of the cap
|
|
94
|
+
# being lifted, without piling up backlog or burning cycles in the gap.
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
SA_PREFLIGHT="$(cd "$(dirname "$0")" && pwd)/preflight.sh"
|
|
97
|
+
SA_QUOTA_PREFLIGHT_OK=0
|
|
98
|
+
if [ -f "$SA_PREFLIGHT" ]; then
|
|
99
|
+
# shellcheck source=/dev/null
|
|
100
|
+
source "$SA_PREFLIGHT"
|
|
101
|
+
SA_QUOTA_PREFLIGHT_OK=1
|
|
102
|
+
# If a prior run stamped a still-valid block, exit 79 with a [skipped:]
|
|
103
|
+
# log line. We don't reuse preflight_skip_if_claude_blocked verbatim
|
|
104
|
+
# because that helper exits 0 (suitable for launchd wrappers); here we
|
|
105
|
+
# want exit 79 so the *caller pipeline* can tell "claude blocked" from
|
|
106
|
+
# "claude ran cleanly but returned no candidates".
|
|
107
|
+
if [ -f "$SA_CLAUDE_BLOCK_STAMP" ]; then
|
|
108
|
+
SA_BLOCK_REMAINING=$(/usr/bin/python3 - "$SA_CLAUDE_BLOCK_STAMP" <<'PY'
|
|
109
|
+
import json, sys
|
|
110
|
+
from datetime import datetime, timezone
|
|
111
|
+
try:
|
|
112
|
+
with open(sys.argv[1]) as f:
|
|
113
|
+
d = json.load(f)
|
|
114
|
+
bu = d.get("blocked_until", "")
|
|
115
|
+
if not bu:
|
|
116
|
+
print(0); sys.exit(0)
|
|
117
|
+
until = datetime.fromisoformat(bu.replace("Z", "+00:00"))
|
|
118
|
+
now = datetime.now(timezone.utc)
|
|
119
|
+
print(int(max(0, (until - now).total_seconds())))
|
|
120
|
+
except Exception:
|
|
121
|
+
print(0)
|
|
122
|
+
PY
|
|
123
|
+
)
|
|
124
|
+
if [ "${SA_BLOCK_REMAINING:-0}" -gt 0 ]; then
|
|
125
|
+
SA_BLOCK_REASON=$(/usr/bin/python3 -c "import json; print(json.load(open('$SA_CLAUDE_BLOCK_STAMP')).get('reason','unknown'))" 2>/dev/null)
|
|
126
|
+
echo "[run_claude] skipped: claude_blocked reason=$SA_BLOCK_REASON expires_in=${SA_BLOCK_REMAINING}s script=$SCRIPT_TAG; exit 79" >&2
|
|
127
|
+
exit 79
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# Auto-detect the platform agent from --mcp-config and signal the PreToolUse
|
|
133
|
+
# hooks (~/.claude/hooks/<platform>-agent-lock.sh) to bypass the cross-session
|
|
134
|
+
# block check. Rationale: every caller of run_claude.sh inside this repo is a
|
|
135
|
+
# launchd-managed pipeline that has already acquired the shell-level
|
|
136
|
+
# <platform>-browser lock via skill/lock.sh BEFORE invoking us. The shell
|
|
137
|
+
# lock is the authoritative serializer; the hook lock used to layer a second
|
|
138
|
+
# block on top, which produced false positives like the 2026-05-01 14:33
|
|
139
|
+
# LinkedIn run that paid $8.91 for an empty envelope because the *prior*
|
|
140
|
+
# LinkedIn cycle's JSONL was 57s stale (under the hook's 60s threshold)
|
|
141
|
+
# even though the shell lock had cleanly released.
|
|
142
|
+
#
|
|
143
|
+
# When SA_PIPELINE_LOCKED=1 is set, the hook trusts the shell layer and
|
|
144
|
+
# skips the cross-session check entirely.
|
|
145
|
+
for arg in "$@"; do
|
|
146
|
+
case "$arg" in
|
|
147
|
+
*linkedin-agent-mcp.json) export SA_PIPELINE_PLATFORM="linkedin"; export SA_PIPELINE_LOCKED=1 ;;
|
|
148
|
+
*twitter-harness-mcp.json) export SA_PIPELINE_PLATFORM="twitter"; export SA_PIPELINE_LOCKED=1 ;;
|
|
149
|
+
*reddit-agent-mcp.json) export SA_PIPELINE_PLATFORM="reddit"; export SA_PIPELINE_LOCKED=1 ;;
|
|
150
|
+
esac
|
|
151
|
+
done
|
|
152
|
+
|
|
153
|
+
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
154
|
+
START=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
|
|
155
|
+
|
|
156
|
+
# Allow one-off model override without touching locked scripts.
|
|
157
|
+
MODEL_ARGS=()
|
|
158
|
+
if [ -n "${MODEL_OVERRIDE:-}" ]; then
|
|
159
|
+
MODEL_ARGS=(--model "$MODEL_OVERRIDE")
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# Tee claude's stdout to a side file so we can extract the native SDK cost
|
|
163
|
+
# (streamRes.total_cost_usd) emitted in the final result event of stream-json /
|
|
164
|
+
# json output. Stdout still flows unchanged to whoever piped this wrapper, so
|
|
165
|
+
# downstream parsers see identical bytes. PIPESTATUS[0] preserves claude's
|
|
166
|
+
# exit code through the tee.
|
|
167
|
+
SIDE_LOG="$(mktemp -t sa_run_claude_stdout.XXXXXX)"
|
|
168
|
+
|
|
169
|
+
# Active-session sidecar. Lets the dashboard surface a live JSONL-tail link
|
|
170
|
+
# for the in-flight `claude` invocation while the phase is running, and lets
|
|
171
|
+
# investigators find the right transcript when run_claude.sh gets killed by
|
|
172
|
+
# the watchdog before log_claude_session.py can archive it. The file name is
|
|
173
|
+
# the wrapper PID so /api/claude-active can GC stale entries by checking
|
|
174
|
+
# whether the owning process is still alive.
|
|
175
|
+
ACTIVE_DIR="/tmp/sa-active-claude"
|
|
176
|
+
mkdir -p "$ACTIVE_DIR" 2>/dev/null || true
|
|
177
|
+
ACTIVE_FILE="$ACTIVE_DIR/$$.json"
|
|
178
|
+
|
|
179
|
+
# Process-group ID of the most recent claude invocation. Set by the run loop
|
|
180
|
+
# below (we run claude inside a `set -m` brace group + `&` so the forked
|
|
181
|
+
# subshell PID == its PGID, and claude + any grandchildren it spawns inherit
|
|
182
|
+
# that PGID). Used by _sa_cleanup to nuke orphan grandchildren that survive
|
|
183
|
+
# claude itself (e.g. a `find /` claude launched in the background and never
|
|
184
|
+
# waited on, which on 2026-05-01 burned CPU on PID 3187 long after the
|
|
185
|
+
# orchestrator exited because nothing was responsible for cleaning up after
|
|
186
|
+
# claude's kids).
|
|
187
|
+
CLAUDE_PG=""
|
|
188
|
+
|
|
189
|
+
# After-claude cleanup: explicitly remove the hook-layer lockfile for this
|
|
190
|
+
# session so the NEXT pipeline cycle doesn't see a stale lock from us. The
|
|
191
|
+
# unlock hook (PostToolUse) refreshes the lock timestamp to keep it alive
|
|
192
|
+
# across multi-tool sessions; without an explicit final cleanup, the lock
|
|
193
|
+
# survives session end and (per JSONL-mtime check) reads as "live" for up
|
|
194
|
+
# to 60s after we exit, causing the false-positive that produced the
|
|
195
|
+
# 2026-05-01 14:33 $8.91 empty-envelope run.
|
|
196
|
+
_sa_cleanup() {
|
|
197
|
+
rm -f "$SIDE_LOG"
|
|
198
|
+
rm -f "$ACTIVE_FILE"
|
|
199
|
+
|
|
200
|
+
# Sweep orphan claude descendants. Process groups survive the parent's
|
|
201
|
+
# death (kids reparented to launchd keep their PGID), so killing
|
|
202
|
+
# `kill -- -PGID` reaches every grandchild, including ones reparented
|
|
203
|
+
# to PID 1. Done before the lockfile cleanup so any orphan still
|
|
204
|
+
# holding a browser lock dies first.
|
|
205
|
+
if [ -n "$CLAUDE_PG" ]; then
|
|
206
|
+
local survivors
|
|
207
|
+
survivors=$(pgrep -g "$CLAUDE_PG" 2>/dev/null | grep -v "^$$\$" || true)
|
|
208
|
+
if [ -n "$survivors" ]; then
|
|
209
|
+
echo "[run_claude] sweeping orphan claude descendants in pg=$CLAUDE_PG: $(echo $survivors | tr '\n' ' ')" >&2
|
|
210
|
+
kill -TERM -- -"$CLAUDE_PG" 2>/dev/null || true
|
|
211
|
+
sleep 0.3
|
|
212
|
+
kill -KILL -- -"$CLAUDE_PG" 2>/dev/null || true
|
|
213
|
+
fi
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
if [ -n "${SA_PIPELINE_PLATFORM:-}" ]; then
|
|
217
|
+
local lockfile="$HOME/.claude/${SA_PIPELINE_PLATFORM}-agent-lock.json"
|
|
218
|
+
if [ -f "$lockfile" ]; then
|
|
219
|
+
# Only remove if WE hold it — defensive in case a peer raced in.
|
|
220
|
+
local holder
|
|
221
|
+
holder=$(jq -r '.session_id // empty' "$lockfile" 2>/dev/null || echo "")
|
|
222
|
+
if [ "$holder" = "$SESSION_ID" ]; then
|
|
223
|
+
rm -f "$lockfile"
|
|
224
|
+
fi
|
|
225
|
+
fi
|
|
226
|
+
fi
|
|
227
|
+
}
|
|
228
|
+
# Cover EXIT (normal/return from script), INT (Ctrl-C from interactive),
|
|
229
|
+
# TERM (watchdog SIGTERM from scripts/watchdog_hung_runs.py), and HUP
|
|
230
|
+
# (controlling-tty death). SIGKILL is uncatchable; the active sidecar
|
|
231
|
+
# self-GCs on read in that case.
|
|
232
|
+
trap _sa_cleanup EXIT INT TERM HUP
|
|
233
|
+
|
|
234
|
+
# AUP-refusal retry loop. The Claude API safety filter occasionally refuses
|
|
235
|
+
# Phase A / SERP-driven prompts non-deterministically (the same prompt that
|
|
236
|
+
# refused at 18:13 succeeded at 17:58 today, 2026-05-01). Refusal output
|
|
237
|
+
# format: "API Error: Claude Code is unable to respond to this request,
|
|
238
|
+
# which appears to violate our Usage Policy". Retry up to 2 more times with
|
|
239
|
+
# 30s / 60s backoff and a fresh session UUID each retry (the prior session
|
|
240
|
+
# may have been flagged backend-side). Other RC failures pass through.
|
|
241
|
+
: > "$SIDE_LOG"
|
|
242
|
+
MAX_AUP_RETRIES=2
|
|
243
|
+
AUP_BACKOFF=(30 60)
|
|
244
|
+
# Transient-failure retry. The `claude` CLI gets reinstalled
|
|
245
|
+
# periodically (npm/curl installer), and the install briefly removes
|
|
246
|
+
# the old binary before writing the new one. Any invocation in that
|
|
247
|
+
# window gets exit 127 (command not found). Retry up to 3 times with
|
|
248
|
+
# 5s/10s/20s backoff before giving up. These retries do NOT count
|
|
249
|
+
# against the AUP-refusal budget, since the binary never actually ran.
|
|
250
|
+
# Caused two failed runs on 2026-05-01: 19:33 (engage-dm-replies, mid
|
|
251
|
+
# v2.1.126 install) and again on a follow-up cycle.
|
|
252
|
+
MAX_TRANSIENT_RETRIES=3
|
|
253
|
+
TRANSIENT_BACKOFF=(5 10 20)
|
|
254
|
+
RC=0
|
|
255
|
+
attempt=0
|
|
256
|
+
while :; do
|
|
257
|
+
attempt=$((attempt + 1))
|
|
258
|
+
transient_attempt=0
|
|
259
|
+
while :; do
|
|
260
|
+
: > "$SIDE_LOG"
|
|
261
|
+
|
|
262
|
+
# Refresh active-session sidecar each attempt — SESSION_ID rotates on
|
|
263
|
+
# AUP retries (line ~140), so the dashboard always points at the live
|
|
264
|
+
# transcript, not the abandoned one.
|
|
265
|
+
cat > "$ACTIVE_FILE" <<EOF
|
|
266
|
+
{
|
|
267
|
+
"session_id": "$SESSION_ID",
|
|
268
|
+
"script_tag": "$SCRIPT_TAG",
|
|
269
|
+
"wrapper_pid": $$,
|
|
270
|
+
"started_at": "$START",
|
|
271
|
+
"attempt": $attempt,
|
|
272
|
+
"platform": "${SA_PIPELINE_PLATFORM:-}"
|
|
273
|
+
}
|
|
274
|
+
EOF
|
|
275
|
+
|
|
276
|
+
# Run claude in its own process group so we can kill orphans on exit.
|
|
277
|
+
# `set -m` makes background jobs each get their own PGID == job PID;
|
|
278
|
+
# the brace-group pipeline runs in a forked subshell whose PID is the
|
|
279
|
+
# PGID, and claude inherits that PGID along with any descendants it
|
|
280
|
+
# spawns. PIPESTATUS is captured INSIDE the brace group so the
|
|
281
|
+
# subshell's exit code IS claude's exit code (not tee's), giving us
|
|
282
|
+
# the same exit semantics callers had before.
|
|
283
|
+
#
|
|
284
|
+
# BASH-VERSION CAVEAT: this whole pattern depends on bash putting each
|
|
285
|
+
# backgrounded job in its own process group when `set -m` is on, AND
|
|
286
|
+
# on `$!` returning the brace-group subshell's PID (which equals the
|
|
287
|
+
# new PGID). Verified on macOS bash 3.2 (the system default, 2026-05).
|
|
288
|
+
# If the system bash is ever upgraded (bash 5.x, ble.sh wrappers,
|
|
289
|
+
# `shopt -s lastpipe`, `set -o pipefail` toggles, the `inherit_errexit`
|
|
290
|
+
# option, or running under zsh-as-bash compatibility mode), re-verify
|
|
291
|
+
# the orphan sweep with /tmp/sa_pg_test.sh before assuming PGIDs still
|
|
292
|
+
# line up. Specifically check:
|
|
293
|
+
# 1. `pgrep -g $CLAUDE_PG` finds claude + grandchildren mid-run.
|
|
294
|
+
# 2. A `nohup ... &` grandchild that survives claude's exit still
|
|
295
|
+
# shows up in pgrep -g $CLAUDE_PG until the EXIT trap fires.
|
|
296
|
+
# 3. `kill -- -$CLAUDE_PG` actually reaches reparented (PPID=1)
|
|
297
|
+
# orphans — some shells silently strip job-control and put
|
|
298
|
+
# everything in the parent shell's PG, which would make the
|
|
299
|
+
# cleanup nuke the WRONG PG and either kill our own shell or
|
|
300
|
+
# no-op while the orphan keeps running.
|
|
301
|
+
# The 2026-05-01 PID 3187 incident was the symptom we built this
|
|
302
|
+
# against; if it ever returns, this assumption breaking is the
|
|
303
|
+
# first thing to suspect.
|
|
304
|
+
set -m
|
|
305
|
+
{ claude --session-id "$SESSION_ID" ${MODEL_ARGS[@]+"${MODEL_ARGS[@]}"} "$@" | tee -a "$SIDE_LOG"; exit "${PIPESTATUS[0]}"; } &
|
|
306
|
+
CLAUDE_PG=$!
|
|
307
|
+
set +m
|
|
308
|
+
wait "$CLAUDE_PG"
|
|
309
|
+
RC=$?
|
|
310
|
+
if [ "$RC" -ne 127 ]; then
|
|
311
|
+
break
|
|
312
|
+
fi
|
|
313
|
+
if [ "$transient_attempt" -ge "$MAX_TRANSIENT_RETRIES" ]; then
|
|
314
|
+
echo "[run_claude] claude binary still missing after $MAX_TRANSIENT_RETRIES retries; giving up with exit 127" >&2
|
|
315
|
+
break
|
|
316
|
+
fi
|
|
317
|
+
sleep_secs="${TRANSIENT_BACKOFF[$transient_attempt]:-20}"
|
|
318
|
+
transient_attempt=$((transient_attempt + 1))
|
|
319
|
+
echo "[run_claude] claude not found (exit 127, likely mid-reinstall); retrying in ${sleep_secs}s ($transient_attempt/$MAX_TRANSIENT_RETRIES)" >&2
|
|
320
|
+
sleep "$sleep_secs"
|
|
321
|
+
done
|
|
322
|
+
if grep -qE "(API Error|Error).*Usage Policy|appears to violate our Usage Policy" "$SIDE_LOG"; then
|
|
323
|
+
if [ "$attempt" -le "$MAX_AUP_RETRIES" ]; then
|
|
324
|
+
sleep_secs="${AUP_BACKOFF[$((attempt - 1))]:-60}"
|
|
325
|
+
echo "[run_claude] AUP refusal on attempt $attempt/$((MAX_AUP_RETRIES + 1)); retrying in ${sleep_secs}s with new session" >&2
|
|
326
|
+
sleep "$sleep_secs"
|
|
327
|
+
SESSION_ID="$(uuidgen | tr 'A-Z' 'a-z')"
|
|
328
|
+
export CLAUDE_SESSION_ID="$SESSION_ID"
|
|
329
|
+
# SIDE_LOG reset is handled at the top of the inner transient loop.
|
|
330
|
+
continue
|
|
331
|
+
fi
|
|
332
|
+
echo "[run_claude] AUP refusal on final attempt $attempt; giving up" >&2
|
|
333
|
+
fi
|
|
334
|
+
break
|
|
335
|
+
done
|
|
336
|
+
|
|
337
|
+
END=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
|
|
338
|
+
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
# Post-hoc quota-error detection (added 2026-05-02).
|
|
341
|
+
#
|
|
342
|
+
# Scan claude's stdout for known fatal-quota signals. On match: stamp the
|
|
343
|
+
# shared block file (so subsequent pipelines skip cleanly) and force exit 79.
|
|
344
|
+
# On no match AND a successful claude run, clear any stale stamp — the cap
|
|
345
|
+
# has lifted.
|
|
346
|
+
#
|
|
347
|
+
# Why post-hoc and not via streaming watchdog: claude already wrote the
|
|
348
|
+
# bytes by the time we'd notice, and the orchestrator turn is one shot per
|
|
349
|
+
# wrapper invocation. Stamping at exit + skipping the NEXT fire is cheaper
|
|
350
|
+
# than racing to interrupt the current one. The current run paid for the
|
|
351
|
+
# error already; we just protect the next 10 min of cadence-ticks.
|
|
352
|
+
# ---------------------------------------------------------------------------
|
|
353
|
+
if [ "$SA_QUOTA_PREFLIGHT_OK" = "1" ]; then
|
|
354
|
+
# Skip the regex-based quota classifier when Claude emitted a successful
|
|
355
|
+
# SDK result event. Background: 2026-05-07 the Anthropic x SpaceX news
|
|
356
|
+
# cycle produced tweets containing the literal phrase "5 hour rate limit".
|
|
357
|
+
# Phase 1 of run-twitter-cycle.sh dumps every scraped tweet's full text
|
|
358
|
+
# into stdout as structured_output, the classifier matched the phrase
|
|
359
|
+
# inside tweet bodies, stamped /tmp/sa-claude-blocked.json, and the
|
|
360
|
+
# subsequent Phase 2b-prep skipped with `claude_blocked` even though
|
|
361
|
+
# Claude itself ran cleanly. The shared stamp file then blocked ~12 runs
|
|
362
|
+
# across twitter-cycle, engage-twitter, and run-moltbook before clearing.
|
|
363
|
+
#
|
|
364
|
+
# The result event is a single-line JSON object emitted last, and only
|
|
365
|
+
# on a clean orchestrator turn:
|
|
366
|
+
# {"type":"result","subtype":"success","is_error":false,...}
|
|
367
|
+
# When that line is present AND claude exited 0, the run was clean by
|
|
368
|
+
# construction — there is no quota error to classify. Anchoring on
|
|
369
|
+
# `"subtype":"success"` (not just `is_error:false`) is intentional:
|
|
370
|
+
# tweet bodies may contain `is_error` strings, but `"subtype":"success"`
|
|
371
|
+
# is unique to the final result event.
|
|
372
|
+
SA_RESULT_OK=0
|
|
373
|
+
if [ "$RC" = "0" ] && grep -qE '"type":"result"[^}]*"subtype":"success"' "$SIDE_LOG" 2>/dev/null; then
|
|
374
|
+
SA_RESULT_OK=1
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
SA_QUOTA_REASON=""
|
|
378
|
+
if [ "$SA_RESULT_OK" = "0" ]; then
|
|
379
|
+
SA_QUOTA_REASON="$(preflight_classify_claude_error "$SIDE_LOG" 2>/dev/null | head -1 | tr -d '[:space:]')"
|
|
380
|
+
fi
|
|
381
|
+
|
|
382
|
+
if [ -n "$SA_QUOTA_REASON" ]; then
|
|
383
|
+
# Stamp + force exit 79. Block window 600s (10 min). If the underlying
|
|
384
|
+
# cap is real, the next 10 min of fires skip cleanly. After 600s a
|
|
385
|
+
# fresh fire retries; success clears the stamp, repeat-failure
|
|
386
|
+
# refreshes it.
|
|
387
|
+
preflight_stamp_claude_blocked "$SA_QUOTA_REASON" 600 "$SCRIPT_TAG" "$SESSION_ID"
|
|
388
|
+
echo "[run_claude] quota error detected reason=$SA_QUOTA_REASON; skipping next 10 min of fires (exit 79)" >&2
|
|
389
|
+
# Still log the session for cost accounting before exiting.
|
|
390
|
+
ORCH_COST="$(grep -oE '"total_cost_usd"[[:space:]]*:[[:space:]]*[0-9]+(\.[0-9]+)?' "$SIDE_LOG" 2>/dev/null \
|
|
391
|
+
| tail -1 \
|
|
392
|
+
| sed -E 's/.*:[[:space:]]*//')"
|
|
393
|
+
ORCH_ARGS=()
|
|
394
|
+
if [ -n "$ORCH_COST" ]; then
|
|
395
|
+
ORCH_ARGS=(--orchestrator-cost-usd "$ORCH_COST")
|
|
396
|
+
fi
|
|
397
|
+
/usr/bin/python3 "$REPO_DIR/scripts/log_claude_session.py" \
|
|
398
|
+
--session-id "$SESSION_ID" \
|
|
399
|
+
--script "$SCRIPT_TAG" \
|
|
400
|
+
--started-at "$START" \
|
|
401
|
+
--ended-at "$END" \
|
|
402
|
+
${ORCH_ARGS[@]+"${ORCH_ARGS[@]}"} >&2 || true
|
|
403
|
+
exit 79
|
|
404
|
+
fi
|
|
405
|
+
# Clean run AND no quota signal — clear any stale stamp (cap has lifted).
|
|
406
|
+
if [ "$RC" = "0" ] && [ -f "$SA_CLAUDE_BLOCK_STAMP" ]; then
|
|
407
|
+
preflight_clear_claude_block
|
|
408
|
+
fi
|
|
409
|
+
fi
|
|
410
|
+
|
|
411
|
+
# Pull the LAST total_cost_usd in the stdout (the result event is emitted last
|
|
412
|
+
# in both stream-json and json modes). Tolerant to spaces and floats; defaults
|
|
413
|
+
# to empty when the format doesn't expose a result event (e.g. interactive runs
|
|
414
|
+
# that crash before the result line) so log_claude_session.py just leaves the
|
|
415
|
+
# DB column NULL.
|
|
416
|
+
ORCH_COST="$(grep -oE '"total_cost_usd"[[:space:]]*:[[:space:]]*[0-9]+(\.[0-9]+)?' "$SIDE_LOG" 2>/dev/null \
|
|
417
|
+
| tail -1 \
|
|
418
|
+
| sed -E 's/.*:[[:space:]]*//')"
|
|
419
|
+
|
|
420
|
+
# Best-effort cost logging. Never let logging failures mask the wrapped
|
|
421
|
+
# command's exit code.
|
|
422
|
+
ORCH_ARGS=()
|
|
423
|
+
if [ -n "$ORCH_COST" ]; then
|
|
424
|
+
ORCH_ARGS=(--orchestrator-cost-usd "$ORCH_COST")
|
|
425
|
+
fi
|
|
426
|
+
python3 "$REPO_DIR/scripts/log_claude_session.py" \
|
|
427
|
+
--session-id "$SESSION_ID" \
|
|
428
|
+
--script "$SCRIPT_TAG" \
|
|
429
|
+
--started-at "$START" \
|
|
430
|
+
--ended-at "$END" \
|
|
431
|
+
${ORCH_ARGS[@]+"${ORCH_ARGS[@]}"} >&2 || true
|
|
432
|
+
|
|
433
|
+
exit $RC
|