@m13v/s4l 1.6.197-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -0
- package/SKILL.md +342 -0
- package/bin/cli.js +980 -0
- package/bin/cookie-helper.js +315 -0
- package/bin/platform.js +59 -0
- package/bin/scheduler/index.js +12 -0
- package/bin/scheduler/launchd.js +518 -0
- package/browser-agent-configs/all-agents-mcp.json +68 -0
- package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
- package/browser-agent-configs/linkedin-agent.json +17 -0
- package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
- package/browser-agent-configs/reddit-agent-mcp.json +16 -0
- package/browser-agent-configs/reddit-agent.json +17 -0
- package/browser-agent-configs/twitter-harness-mcp.json +18 -0
- package/config.example.json +45 -0
- package/mcp/dist/index.js +4212 -0
- package/mcp/dist/onboarding.js +200 -0
- package/mcp/dist/panel.html +176 -0
- package/mcp/dist/product-link.html +102 -0
- package/mcp/dist/repo.js +222 -0
- package/mcp/dist/runtime.js +1079 -0
- package/mcp/dist/screencast.js +323 -0
- package/mcp/dist/setup.js +545 -0
- package/mcp/dist/telemetry.js +306 -0
- package/mcp/dist/twitterAuth.js +138 -0
- package/mcp/dist/version.js +271 -0
- package/mcp/dist/version.json +4 -0
- package/mcp/install-runtime.mjs +70 -0
- package/mcp/install.mjs +169 -0
- package/mcp/manifest.json +80 -0
- package/mcp/menubar/dashboard_server.py +213 -0
- package/mcp/menubar/s4l_card.py +1314 -0
- package/mcp/menubar/s4l_log_relay.py +179 -0
- package/mcp/menubar/s4l_menubar.py +2439 -0
- package/mcp/menubar/s4l_state.py +891 -0
- package/mcp/package.json +34 -0
- package/mcp/shared/doctor.cjs +437 -0
- package/mcp/shared/onboarding-ledger.cjs +324 -0
- package/mcp-servers/browser-harness/server.py +968 -0
- package/package.json +160 -0
- package/requirements.txt +20 -0
- package/scripts/_compute_allowlist.py +58 -0
- package/scripts/_db_update.py +20 -0
- package/scripts/_filt.py +9 -0
- package/scripts/_li_notif_match.py +76 -0
- package/scripts/_li_notif_orchestrate.py +126 -0
- package/scripts/_lock_preempt_test.py +60 -0
- package/scripts/_run_icp_precheck.py +57 -0
- package/scripts/a16z_pearx_calendar_reminders.py +99 -0
- package/scripts/account_resolver.py +141 -0
- package/scripts/active_campaigns.py +114 -0
- package/scripts/active_users.py +190 -0
- package/scripts/amplitude_24h_signups.py +468 -0
- package/scripts/amplitude_signups.py +177 -0
- package/scripts/apply_onboarding_selections.py +131 -0
- package/scripts/audience_pages.py +243 -0
- package/scripts/audit_helper.py +120 -0
- package/scripts/author_history_block.py +353 -0
- package/scripts/autopilot_stall_watch.py +284 -0
- package/scripts/backfill_twitter_attempts_topic.py +81 -0
- package/scripts/backfill_twitter_log_post_no_id.py +322 -0
- package/scripts/bench_dashboard.sh +138 -0
- package/scripts/bh_send.py +39 -0
- package/scripts/build_persona.py +409 -0
- package/scripts/bulk_icp.py +18 -0
- package/scripts/campaign_bump.py +51 -0
- package/scripts/capture_thread_media.py +288 -0
- package/scripts/check_browser_lock_health.sh +81 -0
- package/scripts/check_external_pool_depth.py +253 -0
- package/scripts/check_unread_web_chats.py +28 -0
- package/scripts/claim_web_chat.py +47 -0
- package/scripts/classify_run_error.py +158 -0
- package/scripts/claude_job.py +988 -0
- package/scripts/clean_stale_singleton.sh +56 -0
- package/scripts/cleanup_harness_tabs.py +68 -0
- package/scripts/copy_browser_cookies.py +454 -0
- package/scripts/counterparty_history.py +350 -0
- package/scripts/db.py +57 -0
- package/scripts/discover_claude_profiles.py +120 -0
- package/scripts/discover_linkedin_candidates.py +984 -0
- package/scripts/dm_conversation.py +682 -0
- package/scripts/dm_db_update.py +69 -0
- package/scripts/dm_engage_helper.py +161 -0
- package/scripts/dm_outreach_helper.py +147 -0
- package/scripts/dm_outreach_twitter_helper.py +129 -0
- package/scripts/dm_send_log.py +106 -0
- package/scripts/dm_short_links.py +1084 -0
- package/scripts/dump_web_chat_history.py +47 -0
- package/scripts/engage_github.py +640 -0
- package/scripts/engage_reddit.py +1235 -0
- package/scripts/engage_twitter_helper.py +301 -0
- package/scripts/engagement_styles.py +1787 -0
- package/scripts/enrich_twitter_candidates.py +82 -0
- package/scripts/feedback_digest.py +448 -0
- package/scripts/fetch_prospect_profile.py +312 -0
- package/scripts/fetch_twitter_t1.py +134 -0
- package/scripts/find_threads.py +530 -0
- package/scripts/follow_gate_log.py +59 -0
- package/scripts/funnel_per_day.py +194 -0
- package/scripts/generate_daily_human_style.py +494 -0
- package/scripts/generation_trace.py +173 -0
- package/scripts/get_run_cost.py +107 -0
- package/scripts/github_engage_helper.py +93 -0
- package/scripts/github_tools.py +509 -0
- package/scripts/harness_overlay.py +556 -0
- package/scripts/harvest_twitter_following.py +243 -0
- package/scripts/heartbeat.sh +70 -0
- package/scripts/history_context.py +284 -0
- package/scripts/http_api.py +206 -0
- package/scripts/human_dm_replies_helper.py +169 -0
- package/scripts/identity.py +302 -0
- package/scripts/ig_batch_creator.sh +93 -0
- package/scripts/ig_post_type_picker.py +243 -0
- package/scripts/ig_scrape_transcribe.sh +91 -0
- package/scripts/ingest_human_dm_replies.py +271 -0
- package/scripts/ingest_web_chat_replies.py +229 -0
- package/scripts/install_fleet.py +187 -0
- package/scripts/invent_mcp_server.py +350 -0
- package/scripts/invent_topics.py +1462 -0
- package/scripts/learned_preferences.py +263 -0
- package/scripts/li_discovery.py +161 -0
- package/scripts/link_edit_helper.py +142 -0
- package/scripts/link_tail.py +592 -0
- package/scripts/linkedin_api.py +561 -0
- package/scripts/linkedin_browser.py +730 -0
- package/scripts/linkedin_cooldown.py +128 -0
- package/scripts/linkedin_exclusions.py +234 -0
- package/scripts/linkedin_killswitch.py +1333 -0
- package/scripts/linkedin_search_topic_schema.py +49 -0
- package/scripts/linkedin_unipile.py +658 -0
- package/scripts/linkedin_url.py +228 -0
- package/scripts/log_claude_session.py +636 -0
- package/scripts/log_draft.py +143 -0
- package/scripts/log_linkedin_search_attempts.py +126 -0
- package/scripts/log_post.py +651 -0
- package/scripts/log_run.py +364 -0
- package/scripts/log_thread_media.py +108 -0
- package/scripts/log_twitter_search_attempts.py +150 -0
- package/scripts/log_twitter_skips.py +211 -0
- package/scripts/lookup_post.py +78 -0
- package/scripts/mark_web_chat_processed.py +32 -0
- package/scripts/mcp_lock_proxy.py +370 -0
- package/scripts/memory_snapshot.py +972 -0
- package/scripts/merge_review_queue.py +215 -0
- package/scripts/mint_external_pool.py +182 -0
- package/scripts/mint_kent_pool.py +249 -0
- package/scripts/moltbook_post.py +320 -0
- package/scripts/moltbook_tools.py +159 -0
- package/scripts/pending_threads.py +188 -0
- package/scripts/pick_ig_account.py +177 -0
- package/scripts/pick_project.py +208 -0
- package/scripts/pick_search_topic.py +771 -0
- package/scripts/pick_thread_target.py +279 -0
- package/scripts/pick_twitter_thread_target.py +202 -0
- package/scripts/podlog_fetch_batch.sh +32 -0
- package/scripts/post_github.py +1311 -0
- package/scripts/post_reddit.py +2668 -0
- package/scripts/precompute_dashboard_stats.py +204 -0
- package/scripts/preflight.sh +297 -0
- package/scripts/progress.py +88 -0
- package/scripts/project_excludes.py +353 -0
- package/scripts/project_slugs.py +91 -0
- package/scripts/project_stats.py +241 -0
- package/scripts/project_stats_json.py +1563 -0
- package/scripts/project_topics.py +192 -0
- package/scripts/qualified_query_bank.py +436 -0
- package/scripts/reap_stale_claude_sessions.py +867 -0
- package/scripts/reddit_browser.py +2549 -0
- package/scripts/reddit_browser_fetch.py +141 -0
- package/scripts/reddit_browser_lock.py +593 -0
- package/scripts/reddit_chat_sync.py +710 -0
- package/scripts/reddit_query_bank.py +200 -0
- package/scripts/reddit_threads_helper.py +151 -0
- package/scripts/reddit_tools.py +956 -0
- package/scripts/refresh_instagram_tokens.py +280 -0
- package/scripts/release-mcpb.sh +497 -0
- package/scripts/reply_db.py +334 -0
- package/scripts/reply_insert.py +98 -0
- package/scripts/reply_risk_digest.py +761 -0
- package/scripts/reset-test-machine.sh +602 -0
- package/scripts/restore_twitter_session.py +177 -0
- package/scripts/ripen_reddit_plan.py +478 -0
- package/scripts/run_claude.sh +433 -0
- package/scripts/run_moltbook_cycle.py +555 -0
- package/scripts/s4l_box_update.sh +226 -0
- package/scripts/s4l_channel.py +103 -0
- package/scripts/s4l_ctl.sh +75 -0
- package/scripts/s4l_env.py +47 -0
- package/scripts/saps_activity.py +126 -0
- package/scripts/saps_mode.py +328 -0
- package/scripts/scan_dm_candidates.py +580 -0
- package/scripts/scan_github_replies.py +168 -0
- package/scripts/scan_instagram_comments.py +481 -0
- package/scripts/scan_moltbook_replies.py +252 -0
- package/scripts/scan_pii.py +190 -0
- package/scripts/scan_reddit_replies.py +377 -0
- package/scripts/scan_twitter_mentions_browser.py +327 -0
- package/scripts/scan_twitter_thread_followups.py +299 -0
- package/scripts/scan_x_profile.py +384 -0
- package/scripts/schedule_state.py +202 -0
- package/scripts/scheduled_tasks_snapshot.py +123 -0
- package/scripts/score_linkedin_candidates.py +419 -0
- package/scripts/score_twitter_candidates.py +718 -0
- package/scripts/scrape_linkedin_comment_stats.py +1755 -0
- package/scripts/scrape_linkedin_stats_browser.py +52 -0
- package/scripts/scrape_reddit_views.py +365 -0
- package/scripts/seed_search_queries.py +453 -0
- package/scripts/seed_search_topics.py +127 -0
- package/scripts/send_web_chat_reply.py +130 -0
- package/scripts/sentry_init.py +128 -0
- package/scripts/setup_twitter_auth.py +1320 -0
- package/scripts/snapshot.py +583 -0
- package/scripts/stats.py +2702 -0
- package/scripts/stats_helper.py +52 -0
- package/scripts/strike_alert.py +783 -0
- package/scripts/sweep_post_link_clicks.py +107 -0
- package/scripts/sync_ig_to_posts.py +147 -0
- package/scripts/test_browser_lock.py +189 -0
- package/scripts/test_installation_api.sh +52 -0
- package/scripts/test_percard_posting.py +142 -0
- package/scripts/top_dud_linkedin_queries.py +71 -0
- package/scripts/top_dud_reddit_queries.py +67 -0
- package/scripts/top_dud_twitter_queries.py +71 -0
- package/scripts/top_dud_twitter_topics.py +102 -0
- package/scripts/top_linkedin_queries.py +55 -0
- package/scripts/top_omitted_reddit_topics.py +91 -0
- package/scripts/top_performers.py +588 -0
- package/scripts/top_search_topics.py +180 -0
- package/scripts/top_twitter_queries.py +190 -0
- package/scripts/twitter_access_check.py +382 -0
- package/scripts/twitter_account.py +41 -0
- package/scripts/twitter_batch_phase.py +126 -0
- package/scripts/twitter_browser.py +2804 -0
- package/scripts/twitter_cookie_mirror.py +130 -0
- package/scripts/twitter_cycle_helper.py +310 -0
- package/scripts/twitter_gen_links.py +287 -0
- package/scripts/twitter_post_plan.py +1188 -0
- package/scripts/twitter_scan.py +324 -0
- package/scripts/twitter_supply_signal.py +57 -0
- package/scripts/twitter_threads_helper.py +152 -0
- package/scripts/unclaim_web_chat.py +29 -0
- package/scripts/update_instagram_stats.py +261 -0
- package/scripts/update_linkedin_stats_from_feed.py +328 -0
- package/scripts/version.py +72 -0
- package/scripts/watchdog_hung_runs.py +343 -0
- package/scripts/write_generation_trace.py +73 -0
- package/setup/SKILL.md +277 -0
- package/skill/amplitude-24h-signups.sh +38 -0
- package/skill/archive-old-logs.sh +40 -0
- package/skill/audit-dm-staleness.sh +42 -0
- package/skill/audit-linkedin.sh +14 -0
- package/skill/audit-moltbook.sh +4 -0
- package/skill/audit-reddit-resurrect.sh +67 -0
- package/skill/audit-reddit.sh +4 -0
- package/skill/audit-twitter.sh +4 -0
- package/skill/audit.sh +287 -0
- package/skill/backfill-twitter-attempts-topic.sh +19 -0
- package/skill/backfill-twitter-ghost-posts.sh +24 -0
- package/skill/check-external-pool-depth.sh +7 -0
- package/skill/check-web-chats.sh +203 -0
- package/skill/dm-outreach-linkedin.sh +250 -0
- package/skill/dm-outreach-reddit.sh +274 -0
- package/skill/dm-outreach-twitter.sh +265 -0
- package/skill/engage-dm-replies-linkedin.sh +4 -0
- package/skill/engage-dm-replies-reddit.sh +4 -0
- package/skill/engage-dm-replies-twitter.sh +4 -0
- package/skill/engage-dm-replies.sh +1597 -0
- package/skill/engage-linkedin.sh +581 -0
- package/skill/engage-moltbook.sh +36 -0
- package/skill/engage-reddit.sh +146 -0
- package/skill/engage-twitter.sh +467 -0
- package/skill/github-engage.sh +176 -0
- package/skill/ingest-web-chat-replies.sh +38 -0
- package/skill/invent-supply-test.sh +100 -0
- package/skill/invent-topics.sh +50 -0
- package/skill/lib/linkedin-backend.sh +364 -0
- package/skill/lib/platform.sh +48 -0
- package/skill/lib/reddit-backend.sh +234 -0
- package/skill/lib/twitter-backend.sh +314 -0
- package/skill/link-edit-github.sh +136 -0
- package/skill/link-edit-moltbook.sh +117 -0
- package/skill/link-edit-reddit.sh +201 -0
- package/skill/linkedin-presence.sh +182 -0
- package/skill/linkedin-recovery.sh +282 -0
- package/skill/lock.sh +647 -0
- package/skill/memory-snapshot.sh +39 -0
- package/skill/precompute-stats.sh +35 -0
- package/skill/prewarm-funnel.sh +104 -0
- package/skill/refresh-instagram-tokens.sh +57 -0
- package/skill/refresh-twitter-following.sh +52 -0
- package/skill/reply-risk-digest.sh +31 -0
- package/skill/run-cycle-update-guard.sh +44 -0
- package/skill/run-draft-and-publish.sh +123 -0
- package/skill/run-generate-daily-style.sh +50 -0
- package/skill/run-github-launchd.sh +62 -0
- package/skill/run-github.sh +102 -0
- package/skill/run-instagram-daily.sh +149 -0
- package/skill/run-instagram-render.sh +875 -0
- package/skill/run-linkedin-launchd.sh +81 -0
- package/skill/run-linkedin-unipile.sh +130 -0
- package/skill/run-linkedin.sh +1593 -0
- package/skill/run-moltbook-launchd.sh +61 -0
- package/skill/run-moltbook.sh +38 -0
- package/skill/run-overlay-watch.sh +100 -0
- package/skill/run-reddit-search-launchd.sh +64 -0
- package/skill/run-reddit-search.sh +505 -0
- package/skill/run-reddit-threads-double.sh +32 -0
- package/skill/run-reddit-threads.sh +847 -0
- package/skill/run-scan-moltbook-replies.sh +57 -0
- package/skill/run-twitter-cycle-launchd.sh +63 -0
- package/skill/run-twitter-cycle-singleton.sh +62 -0
- package/skill/run-twitter-cycle.sh +2408 -0
- package/skill/run-twitter-threads.sh +592 -0
- package/skill/scan-instagram-replies.sh +61 -0
- package/skill/scan-twitter-followups.sh +57 -0
- package/skill/social-autoposter-update.sh +66 -0
- package/skill/stats-instagram.sh +72 -0
- package/skill/stats-linkedin.sh +271 -0
- package/skill/stats-moltbook.sh +4 -0
- package/skill/stats-reddit.sh +4 -0
- package/skill/stats-twitter.sh +4 -0
- package/skill/stats.sh +521 -0
- package/skill/strike-alert.sh +18 -0
- package/skill/styles.sh +87 -0
- package/skill/sweep-link-clicks.sh +40 -0
- package/skill/topics.sh +51 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""run_moltbook_cycle.py — phased MoltBook posting cycle.
|
|
3
|
+
|
|
4
|
+
Reduces volume by gating on:
|
|
5
|
+
1. Historical (project, style) engagement signal injected into the drafter prompt.
|
|
6
|
+
2. T0 -> T1 momentum gate: scan threads now, sleep 10 min, re-poll, compute delta.
|
|
7
|
+
3. Adaptive cap: default 2 posts/cycle, bump to 5 only when >=3 candidates
|
|
8
|
+
show real-time momentum (delta >= threshold).
|
|
9
|
+
|
|
10
|
+
Phase 1: scan hot + new via API, snapshot T0 engagement (in-memory)
|
|
11
|
+
Sleep: --sleep seconds (default 600)
|
|
12
|
+
Phase 2a: re-poll same threads, compute delta
|
|
13
|
+
Phase 2b: Claude picks from top-N pre-filtered candidates, drafts, Python posts
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
python3 scripts/run_moltbook_cycle.py
|
|
17
|
+
python3 scripts/run_moltbook_cycle.py --sleep 300 --dry-run
|
|
18
|
+
"""
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
import time
|
|
25
|
+
import uuid
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
|
|
28
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
29
|
+
from http_api import api_get, api_post, load_env
|
|
30
|
+
from moltbook_tools import fetch_moltbook_json, MoltbookRateLimitedError
|
|
31
|
+
from engagement_styles import validate_or_register, pick_style_for_post
|
|
32
|
+
from version import read_version as read_autoposter_version
|
|
33
|
+
from project_topics import topics_for_project
|
|
34
|
+
|
|
35
|
+
REPO_DIR = os.path.expanduser("~/social-autoposter")
|
|
36
|
+
SCRIPTS = os.path.join(REPO_DIR, "scripts")
|
|
37
|
+
CONFIG_PATH = os.path.join(REPO_DIR, "config.json")
|
|
38
|
+
SKILL_FILE = os.path.join(REPO_DIR, "SKILL.md")
|
|
39
|
+
MOLTBOOK_POST = os.path.join(SCRIPTS, "moltbook_post.py")
|
|
40
|
+
RUN_CLAUDE = os.path.join(SCRIPTS, "run_claude.sh")
|
|
41
|
+
HISTORICAL = os.path.join(SCRIPTS, "historical_engagement.py")
|
|
42
|
+
|
|
43
|
+
# --- Momentum + cap thresholds (single source of truth, tune here) ----------
|
|
44
|
+
DELTA_THRESHOLD = 5.0 # candidate counts as "high momentum" if delta_score >= this
|
|
45
|
+
HIGH_DELTA_BUMP = 3 # need this many high-momentum candidates to bump cap
|
|
46
|
+
CAP_DEFAULT = 1
|
|
47
|
+
CAP_BUMPED = 1
|
|
48
|
+
CLAUDE_CANDIDATE_LIMIT = 15 # show at most this many candidates to Claude
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def log(msg):
|
|
52
|
+
print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}", flush=True)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_config():
|
|
56
|
+
with open(CONFIG_PATH) as f:
|
|
57
|
+
return json.load(f)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def api_key():
|
|
61
|
+
k = os.environ.get("MOLTBOOK_API_KEY")
|
|
62
|
+
if k:
|
|
63
|
+
return k
|
|
64
|
+
env_file = os.path.join(REPO_DIR, ".env")
|
|
65
|
+
if os.path.exists(env_file):
|
|
66
|
+
with open(env_file) as f:
|
|
67
|
+
for line in f:
|
|
68
|
+
if line.startswith("MOLTBOOK_API_KEY="):
|
|
69
|
+
return line.strip().split("=", 1)[1]
|
|
70
|
+
print("ERROR: MOLTBOOK_API_KEY not set", file=sys.stderr)
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def fetch_sorted(kind, api_key_, limit=50):
|
|
75
|
+
"""kind: 'hot' or 'new'. Returns list of post dicts."""
|
|
76
|
+
url = f"https://www.moltbook.com/api/v1/posts?sort={kind}&limit={limit}"
|
|
77
|
+
data = fetch_moltbook_json(url, api_key=api_key_)
|
|
78
|
+
if not data:
|
|
79
|
+
return []
|
|
80
|
+
return data.get("posts", []) or data.get("data", []) or []
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def fetch_one(post_id, api_key_):
|
|
84
|
+
"""Re-fetch a single post for T1 measurement."""
|
|
85
|
+
url = f"https://www.moltbook.com/api/v1/posts/{post_id}"
|
|
86
|
+
data = fetch_moltbook_json(url, api_key=api_key_)
|
|
87
|
+
if not data:
|
|
88
|
+
return None
|
|
89
|
+
return data.get("post") or data
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def already_posted_thread_ids(thread_ids):
|
|
93
|
+
"""Return the subset we've already commented on, to exclude.
|
|
94
|
+
|
|
95
|
+
The old single SQL OR-LIKE query is replaced by one posts GET per
|
|
96
|
+
thread_id (thread_url_contains). thread_ids is this cycle's candidate
|
|
97
|
+
set (scan-limit, ~50), so the request count stays bounded.
|
|
98
|
+
"""
|
|
99
|
+
if not thread_ids:
|
|
100
|
+
return set()
|
|
101
|
+
hit = set()
|
|
102
|
+
for tid in thread_ids:
|
|
103
|
+
resp = api_get(
|
|
104
|
+
"/api/v1/posts",
|
|
105
|
+
query={
|
|
106
|
+
"platform": "moltbook",
|
|
107
|
+
"thread_url_contains": tid,
|
|
108
|
+
"limit": 1,
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
rows = ((resp or {}).get("data") or {}).get("posts") or []
|
|
112
|
+
if rows:
|
|
113
|
+
hit.add(tid)
|
|
114
|
+
return hit
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def snapshot(post):
|
|
118
|
+
pid = post.get("id")
|
|
119
|
+
return {
|
|
120
|
+
"id": pid,
|
|
121
|
+
"title": post.get("title", ""),
|
|
122
|
+
"content": (post.get("content") or ""),
|
|
123
|
+
"author": (post.get("user") or {}).get("username") or post.get("author") or "",
|
|
124
|
+
"submolt": (post.get("submolt") or {}).get("name") or post.get("submolt_name") or "",
|
|
125
|
+
"url": f"https://www.moltbook.com/post/{pid}",
|
|
126
|
+
"upvotes_t0": int(post.get("upvote_count") or post.get("upvotes") or 0),
|
|
127
|
+
"comments_t0": int(post.get("comment_count") or post.get("comments_count") or 0),
|
|
128
|
+
"created_at": post.get("created_at") or "",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def delta_score(t0_up, t0_cm, t1_up, t1_cm):
|
|
133
|
+
"""Weight comments higher than upvotes (rarer, stronger signal)."""
|
|
134
|
+
return 2.0 * max(t1_up - t0_up, 0) + 5.0 * max(t1_cm - t0_cm, 0)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def build_prompt(candidates, cap, history_block, styles_block, projects_json):
|
|
138
|
+
cand_block = []
|
|
139
|
+
for i, c in enumerate(candidates, 1):
|
|
140
|
+
cand_block.append(
|
|
141
|
+
f"--- #{i} id={c['id']} delta={c['delta_score']:.1f} "
|
|
142
|
+
f"(up {c['upvotes_t0']}->{c['upvotes_t1']}, "
|
|
143
|
+
f"cm {c['comments_t0']}->{c['comments_t1']}) ---\n"
|
|
144
|
+
f"submolt: {c['submolt']} author: {c['author']}\n"
|
|
145
|
+
f"title: {c['title']}\n"
|
|
146
|
+
f"body: {c['content']}\n"
|
|
147
|
+
f"url: {c['url']}\n"
|
|
148
|
+
)
|
|
149
|
+
candidates_text = "\n".join(cand_block)
|
|
150
|
+
|
|
151
|
+
return f"""You are the Social Autoposter reviewing MoltBook candidates for commenting.
|
|
152
|
+
|
|
153
|
+
Read {SKILL_FILE} for content rules (agent voice, no em dashes, anti-AI).
|
|
154
|
+
|
|
155
|
+
## Pre-filtered candidates (top {len(candidates)} by 10-minute engagement delta)
|
|
156
|
+
|
|
157
|
+
{candidates_text}
|
|
158
|
+
|
|
159
|
+
## Project configs
|
|
160
|
+
{projects_json}
|
|
161
|
+
|
|
162
|
+
{styles_block}
|
|
163
|
+
|
|
164
|
+
{history_block}
|
|
165
|
+
|
|
166
|
+
## YOUR JOB
|
|
167
|
+
|
|
168
|
+
Pick AT MOST {cap} candidates and draft a comment for each. **Post fewer than {cap} if
|
|
169
|
+
fewer than {cap} are genuinely on-brand.** Better to skip than to force a comment.
|
|
170
|
+
|
|
171
|
+
Rules:
|
|
172
|
+
- Skip candidates whose submolt/title are mbc20/crypto/spam or have no plausible angle.
|
|
173
|
+
- For each kept candidate, pick the ONE best-fit project from the config.
|
|
174
|
+
- Choose an engagement_style from the styles block.
|
|
175
|
+
- **Consult the historical engagement table above.** If a (project, style) pair has
|
|
176
|
+
the [dead] label (>=5 past posts, median engagement 0), avoid that pair unless the
|
|
177
|
+
thread is an unusually good fit. Prefer [good] pairs when plausible.
|
|
178
|
+
- Draft the comment in agent voice (\"my human\" not \"I\"), match the thread's language.
|
|
179
|
+
- Apply the matched project's `voice` block: follow `voice.tone`, never violate `voice.never`, mirror `voice.examples` / `voice.examples_good` when present.
|
|
180
|
+
- Comments must add a concrete, thread-relevant point. Do not paste generic product pitches.
|
|
181
|
+
|
|
182
|
+
## OUTPUT FORMAT
|
|
183
|
+
|
|
184
|
+
Return ONLY a single JSON object, no prose, with this exact shape:
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{{
|
|
188
|
+
"posts": [
|
|
189
|
+
{{
|
|
190
|
+
"thread_id": "<candidate id>",
|
|
191
|
+
"thread_url": "<candidate url>",
|
|
192
|
+
"thread_title": "<candidate title>",
|
|
193
|
+
"thread_author": "<candidate author>",
|
|
194
|
+
"matched_project": "<project name from config>",
|
|
195
|
+
"engagement_style": "<one of the valid styles, or your invented name>",
|
|
196
|
+
"new_style": null,
|
|
197
|
+
"language": "<detected language, e.g. en>",
|
|
198
|
+
"comment_text": "<the actual comment to post>"
|
|
199
|
+
}}
|
|
200
|
+
],
|
|
201
|
+
"skipped": [
|
|
202
|
+
{{ "thread_id": "<id>", "reason": "<short reason>" }}
|
|
203
|
+
]
|
|
204
|
+
}}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
If, and ONLY if, none of the listed styles fits, you may invent a new style.
|
|
208
|
+
To do so, set `engagement_style` to your new name (snake_case) AND replace the
|
|
209
|
+
`new_style: null` with a populated block:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
"new_style": {{
|
|
213
|
+
"description": "<what this style is, in one sentence>",
|
|
214
|
+
"example": "<a short example utterance>",
|
|
215
|
+
"note": "<when to use, when not to>",
|
|
216
|
+
"why_existing_didnt_fit": "<which existing style was closest, and why it didn't fit>"
|
|
217
|
+
}}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
If the engagement_style matches one of the listed styles, leave `new_style` as null.
|
|
221
|
+
Inventing should be rare; prefer an existing style if it's even 80% right.
|
|
222
|
+
|
|
223
|
+
CRITICAL: Do NOT call moltbook_post.py or any Bash tool. Only return the JSON.
|
|
224
|
+
The orchestrator will post and log."""
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def parse_claude_json(output):
|
|
228
|
+
# Claude's JSON sits inside a "result" field of its structured output.
|
|
229
|
+
try:
|
|
230
|
+
outer = json.loads(output)
|
|
231
|
+
result = outer.get("result", "") if isinstance(outer, dict) else ""
|
|
232
|
+
except Exception:
|
|
233
|
+
result = output
|
|
234
|
+
# result is a string containing either a JSON object or a fenced ```json block
|
|
235
|
+
m = result
|
|
236
|
+
start = m.find("{")
|
|
237
|
+
if start < 0:
|
|
238
|
+
return None
|
|
239
|
+
depth = 0
|
|
240
|
+
in_str = False
|
|
241
|
+
esc = False
|
|
242
|
+
end = -1
|
|
243
|
+
for i in range(start, len(m)):
|
|
244
|
+
ch = m[i]
|
|
245
|
+
if in_str:
|
|
246
|
+
if esc:
|
|
247
|
+
esc = False
|
|
248
|
+
elif ch == "\\":
|
|
249
|
+
esc = True
|
|
250
|
+
elif ch == '"':
|
|
251
|
+
in_str = False
|
|
252
|
+
continue
|
|
253
|
+
if ch == '"':
|
|
254
|
+
in_str = True
|
|
255
|
+
elif ch == "{":
|
|
256
|
+
depth += 1
|
|
257
|
+
elif ch == "}":
|
|
258
|
+
depth -= 1
|
|
259
|
+
if depth == 0:
|
|
260
|
+
end = i
|
|
261
|
+
break
|
|
262
|
+
if end < 0:
|
|
263
|
+
return None
|
|
264
|
+
try:
|
|
265
|
+
return json.loads(m[start : end + 1])
|
|
266
|
+
except Exception:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def post_and_log(decisions, claude_session_id):
|
|
271
|
+
"""Iterate Claude's picks, call moltbook_post.py, log each to DB."""
|
|
272
|
+
posted = 0
|
|
273
|
+
failed = 0
|
|
274
|
+
|
|
275
|
+
for p in decisions.get("posts", []):
|
|
276
|
+
tid = p.get("thread_id")
|
|
277
|
+
text = p.get("comment_text", "").strip()
|
|
278
|
+
if not tid or not text:
|
|
279
|
+
failed += 1
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
proc = subprocess.run(
|
|
284
|
+
["python3", MOLTBOOK_POST, "comment", "--post-id", tid, "--content", text],
|
|
285
|
+
capture_output=True, text=True, timeout=120,
|
|
286
|
+
)
|
|
287
|
+
except Exception as e:
|
|
288
|
+
log(f" post error for {tid}: {e}")
|
|
289
|
+
failed += 1
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
if proc.returncode != 0:
|
|
293
|
+
log(f" post failed rc={proc.returncode} for {tid}: {proc.stderr.strip()[:200]}")
|
|
294
|
+
failed += 1
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
# moltbook_post prints a final JSON line with url + comment_id
|
|
298
|
+
our_url = ""
|
|
299
|
+
for line in reversed(proc.stdout.strip().splitlines()):
|
|
300
|
+
line = line.strip()
|
|
301
|
+
if line.startswith("{"):
|
|
302
|
+
try:
|
|
303
|
+
js = json.loads(line)
|
|
304
|
+
our_url = js.get("url", "")
|
|
305
|
+
break
|
|
306
|
+
except Exception:
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
# Validate or register the engagement_style. In USE mode any drifted
|
|
310
|
+
# style label is coerced back to style_assignment["style"]; in INVENT
|
|
311
|
+
# mode the new_style block is registered into
|
|
312
|
+
# engagement_styles_registry via the s4l API (replaces the legacy
|
|
313
|
+
# file-based sidecar). The picker's choice is set once for the whole
|
|
314
|
+
# batch above.
|
|
315
|
+
validated_style, style_action = validate_or_register(
|
|
316
|
+
p,
|
|
317
|
+
source_post={
|
|
318
|
+
"platform": "moltbook",
|
|
319
|
+
"post_url": our_url or p.get("thread_url", ""),
|
|
320
|
+
"post_id": None,
|
|
321
|
+
"model": p.get("model"),
|
|
322
|
+
},
|
|
323
|
+
assigned_style=(style_assignment or {}).get("style"),
|
|
324
|
+
assigned_mode=(style_assignment or {}).get("mode"),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# POST /api/v1/posts requires a valid http(s) our_url for active rows
|
|
328
|
+
# (it derives thread_author_handle from thread_author and hardcodes
|
|
329
|
+
# feedback_report_used=TRUE, so those are omitted here). A blank
|
|
330
|
+
# our_url would 400 and crash the loop; the comment is already live,
|
|
331
|
+
# so on the rare parse miss we log a warning and skip the DB row
|
|
332
|
+
# rather than abort the cycle. `project` is the endpoint's key name.
|
|
333
|
+
if not our_url:
|
|
334
|
+
log(f" WARNING: posted to {tid} but could not parse our_url; "
|
|
335
|
+
f"skipping DB log for this row")
|
|
336
|
+
posted += 1
|
|
337
|
+
continue
|
|
338
|
+
api_post(
|
|
339
|
+
"/api/v1/posts",
|
|
340
|
+
{
|
|
341
|
+
"platform": "moltbook",
|
|
342
|
+
"thread_url": p.get("thread_url", ""),
|
|
343
|
+
"thread_author": p.get("thread_author", "various"),
|
|
344
|
+
"thread_title": p.get("thread_title", ""),
|
|
345
|
+
"thread_content": "",
|
|
346
|
+
"our_url": our_url,
|
|
347
|
+
"our_content": text,
|
|
348
|
+
"our_account": "matthew-autoposter",
|
|
349
|
+
"source_summary": "moltbook cycle comment",
|
|
350
|
+
"project": p.get("matched_project", ""),
|
|
351
|
+
"engagement_style": validated_style or "",
|
|
352
|
+
"language": p.get("language", "en"),
|
|
353
|
+
"status": "active",
|
|
354
|
+
"claude_session_id": claude_session_id,
|
|
355
|
+
"autoposter_version": read_autoposter_version(),
|
|
356
|
+
},
|
|
357
|
+
ok_on_conflict=True,
|
|
358
|
+
)
|
|
359
|
+
posted += 1
|
|
360
|
+
style_tag = validated_style or "(none)"
|
|
361
|
+
if style_action == "registered":
|
|
362
|
+
style_tag += " [REGISTERED candidate]"
|
|
363
|
+
log(f" posted to {tid} project={p.get('matched_project')} style={style_tag}")
|
|
364
|
+
|
|
365
|
+
return posted, failed
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def main():
|
|
369
|
+
parser = argparse.ArgumentParser()
|
|
370
|
+
parser.add_argument("--sleep", type=int, default=600)
|
|
371
|
+
parser.add_argument("--scan-limit", type=int, default=50)
|
|
372
|
+
parser.add_argument("--dry-run", action="store_true")
|
|
373
|
+
args = parser.parse_args()
|
|
374
|
+
|
|
375
|
+
run_start = time.time()
|
|
376
|
+
log(f"=== MoltBook Cycle: sleep={args.sleep}s, scan-limit={args.scan_limit} ===")
|
|
377
|
+
|
|
378
|
+
# --- Phase 0: context ---------------------------------------------------
|
|
379
|
+
config = load_config()
|
|
380
|
+
def _project_record(p):
|
|
381
|
+
rec = {k: p.get(k) for k in ("description", "website", "voice")}
|
|
382
|
+
rec["search_topics"] = list(topics_for_project(p.get("name") or ""))
|
|
383
|
+
return rec
|
|
384
|
+
projects_json = json.dumps(
|
|
385
|
+
{p["name"]: _project_record(p)
|
|
386
|
+
for p in config.get("projects", [])
|
|
387
|
+
if p.get("weight", 0) > 0
|
|
388
|
+
and "moltbook" not in (p.get("platforms_disabled") or [])},
|
|
389
|
+
indent=2,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
history_block = subprocess.run(
|
|
394
|
+
["python3", HISTORICAL, "--platform", "moltbook"],
|
|
395
|
+
capture_output=True, text=True, timeout=30,
|
|
396
|
+
).stdout
|
|
397
|
+
except Exception:
|
|
398
|
+
history_block = "## Historical engagement\n(unavailable)\n"
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
styles_block = subprocess.run(
|
|
402
|
+
["bash", "-c", f"source {REPO_DIR}/skill/styles.sh && generate_styles_block moltbook posting"],
|
|
403
|
+
capture_output=True, text=True, timeout=15,
|
|
404
|
+
).stdout
|
|
405
|
+
except Exception:
|
|
406
|
+
styles_block = ""
|
|
407
|
+
|
|
408
|
+
key = api_key()
|
|
409
|
+
|
|
410
|
+
# --- Phase 1: scan T0 ---------------------------------------------------
|
|
411
|
+
log("Phase 1: scanning MoltBook hot + new...")
|
|
412
|
+
try:
|
|
413
|
+
hot = fetch_sorted("hot", key, limit=args.scan_limit)
|
|
414
|
+
new = fetch_sorted("new", key, limit=args.scan_limit)
|
|
415
|
+
except MoltbookRateLimitedError as e:
|
|
416
|
+
log(f"MoltBook rate-limited, aborting cycle: {e.reset_seconds}s")
|
|
417
|
+
return 2
|
|
418
|
+
|
|
419
|
+
seen = {}
|
|
420
|
+
for p in (hot + new):
|
|
421
|
+
snap = snapshot(p)
|
|
422
|
+
if snap["id"] and snap["id"] not in seen:
|
|
423
|
+
seen[snap["id"]] = snap
|
|
424
|
+
|
|
425
|
+
candidates = list(seen.values())
|
|
426
|
+
log(f"Phase 1: {len(candidates)} unique candidates scanned.")
|
|
427
|
+
|
|
428
|
+
# Exclude threads we've already commented on
|
|
429
|
+
posted_before = already_posted_thread_ids([c["id"] for c in candidates])
|
|
430
|
+
candidates = [c for c in candidates if c["id"] not in posted_before]
|
|
431
|
+
log(f"Phase 1: {len(candidates)} after excluding already-posted ({len(posted_before)} filtered).")
|
|
432
|
+
|
|
433
|
+
if not candidates:
|
|
434
|
+
log("No candidates. Exiting.")
|
|
435
|
+
return 0
|
|
436
|
+
|
|
437
|
+
# --- Sleep --------------------------------------------------------------
|
|
438
|
+
log(f"Sleeping {args.sleep}s before T1 re-measurement...")
|
|
439
|
+
time.sleep(args.sleep)
|
|
440
|
+
|
|
441
|
+
# --- Phase 2a: re-poll T1 ----------------------------------------------
|
|
442
|
+
log("Phase 2a: re-polling T1 engagement...")
|
|
443
|
+
for c in candidates:
|
|
444
|
+
try:
|
|
445
|
+
t1 = fetch_one(c["id"], key)
|
|
446
|
+
except MoltbookRateLimitedError as e:
|
|
447
|
+
log(f" rate-limited mid re-poll ({e.reset_seconds}s), using T0 data for remaining")
|
|
448
|
+
break
|
|
449
|
+
if not t1:
|
|
450
|
+
c["upvotes_t1"] = c["upvotes_t0"]
|
|
451
|
+
c["comments_t1"] = c["comments_t0"]
|
|
452
|
+
c["delta_score"] = 0.0
|
|
453
|
+
continue
|
|
454
|
+
c["upvotes_t1"] = int(t1.get("upvote_count") or t1.get("upvotes") or c["upvotes_t0"])
|
|
455
|
+
c["comments_t1"] = int(t1.get("comment_count") or t1.get("comments_count") or c["comments_t0"])
|
|
456
|
+
c["delta_score"] = delta_score(c["upvotes_t0"], c["comments_t0"], c["upvotes_t1"], c["comments_t1"])
|
|
457
|
+
|
|
458
|
+
for c in candidates:
|
|
459
|
+
c.setdefault("upvotes_t1", c["upvotes_t0"])
|
|
460
|
+
c.setdefault("comments_t1", c["comments_t0"])
|
|
461
|
+
c.setdefault("delta_score", 0.0)
|
|
462
|
+
|
|
463
|
+
# --- Phase 2b: adaptive cap + Claude ------------------------------------
|
|
464
|
+
high_delta = [c for c in candidates if c["delta_score"] >= DELTA_THRESHOLD]
|
|
465
|
+
cap = CAP_BUMPED if len(high_delta) >= HIGH_DELTA_BUMP else CAP_DEFAULT
|
|
466
|
+
log(f"Phase 2b: {len(high_delta)} candidates with delta >= {DELTA_THRESHOLD} "
|
|
467
|
+
f"-> cap = {cap}")
|
|
468
|
+
|
|
469
|
+
candidates.sort(key=lambda c: c["delta_score"], reverse=True)
|
|
470
|
+
top = candidates[:CLAUDE_CANDIDATE_LIMIT]
|
|
471
|
+
log(f"Phase 2b: showing Claude top {len(top)} by delta, cap = {cap}")
|
|
472
|
+
for c in top:
|
|
473
|
+
log(f" #{c['id']} delta={c['delta_score']:.1f} "
|
|
474
|
+
f"t0={c['upvotes_t0']}up/{c['comments_t0']}cm "
|
|
475
|
+
f"t1={c['upvotes_t1']}up/{c['comments_t1']}cm")
|
|
476
|
+
|
|
477
|
+
if args.dry_run:
|
|
478
|
+
log("Dry run: skipping Claude + post.")
|
|
479
|
+
for c in top[:cap]:
|
|
480
|
+
log(f" would consider #{c['id']} delta={c['delta_score']:.1f} title={c['title'][:60]}")
|
|
481
|
+
return 0
|
|
482
|
+
|
|
483
|
+
claude_session_id = str(uuid.uuid4())
|
|
484
|
+
os.environ["CLAUDE_SESSION_ID"] = claude_session_id
|
|
485
|
+
# 2026-05-22: pick the engagement style for this draft batch so
|
|
486
|
+
# validate_or_register can coerce any drifted engagement_style label
|
|
487
|
+
# back to the picker's choice. Moltbook batches share one assignment
|
|
488
|
+
# per cycle (same pattern as github batches; cycles run often enough
|
|
489
|
+
# that the picker's distribution averages out). The styles_block in
|
|
490
|
+
# the prompt still shows the legacy menu because the prompt is built
|
|
491
|
+
# by a shell helper; the enforcement happens at the validate step.
|
|
492
|
+
style_assignment = pick_style_for_post("moltbook", context="posting")
|
|
493
|
+
log(f"Style assignment for this batch: mode={style_assignment.get('mode')} "
|
|
494
|
+
f"style={style_assignment.get('style') or '(invent)'}")
|
|
495
|
+
prompt = build_prompt(top, cap, history_block, styles_block, projects_json)
|
|
496
|
+
|
|
497
|
+
log("Phase 2b: invoking Claude for drafting...")
|
|
498
|
+
try:
|
|
499
|
+
proc = subprocess.run(
|
|
500
|
+
[RUN_CLAUDE, "run-moltbook-cycle",
|
|
501
|
+
"--strict-mcp-config",
|
|
502
|
+
"--mcp-config", os.path.expanduser("~/.claude/browser-agent-configs/no-agents-mcp.json"),
|
|
503
|
+
"-p", "--output-format", "json", prompt],
|
|
504
|
+
capture_output=True, text=True, timeout=900,
|
|
505
|
+
)
|
|
506
|
+
except subprocess.TimeoutExpired:
|
|
507
|
+
log("Claude timed out after 900s")
|
|
508
|
+
return 1
|
|
509
|
+
|
|
510
|
+
if proc.returncode != 0:
|
|
511
|
+
log(f"Claude exited rc={proc.returncode}: {proc.stderr[-500:]}")
|
|
512
|
+
return 1
|
|
513
|
+
|
|
514
|
+
decisions = parse_claude_json(proc.stdout)
|
|
515
|
+
if not decisions:
|
|
516
|
+
log("Could not parse Claude JSON output.")
|
|
517
|
+
log(f"Last 500 chars of output: {proc.stdout[-500:]}")
|
|
518
|
+
return 1
|
|
519
|
+
|
|
520
|
+
log(f"Claude picked {len(decisions.get('posts', []))} posts, "
|
|
521
|
+
f"skipped {len(decisions.get('skipped', []))}.")
|
|
522
|
+
|
|
523
|
+
posted, failed = post_and_log(decisions, claude_session_id)
|
|
524
|
+
|
|
525
|
+
elapsed = int(time.time() - run_start)
|
|
526
|
+
log(f"=== Cycle complete: posted={posted}, failed={failed}, elapsed={elapsed}s ===")
|
|
527
|
+
|
|
528
|
+
# Fetch real Claude cost from the session we ran (orchestrator/SDK billing).
|
|
529
|
+
cycle_cost = 0.0
|
|
530
|
+
try:
|
|
531
|
+
_resp = api_get(
|
|
532
|
+
"/api/v1/claude-sessions/cost",
|
|
533
|
+
query={"session_id": claude_session_id},
|
|
534
|
+
)
|
|
535
|
+
cycle_cost = float(((_resp or {}).get("data") or {}).get("parent_cost") or 0.0)
|
|
536
|
+
except Exception as _e:
|
|
537
|
+
log(f"WARNING: could not fetch session cost: {_e}")
|
|
538
|
+
|
|
539
|
+
# Log cycle summary to the run tracking table
|
|
540
|
+
try:
|
|
541
|
+
subprocess.run(
|
|
542
|
+
["python3", os.path.join(SCRIPTS, "log_run.py"),
|
|
543
|
+
"--script", "run-moltbook-cycle",
|
|
544
|
+
"--posted", str(posted), "--skipped", str(len(decisions.get("skipped", []))),
|
|
545
|
+
"--failed", str(failed), "--cost", f"{cycle_cost:.4f}", "--elapsed", str(elapsed)],
|
|
546
|
+
timeout=15,
|
|
547
|
+
)
|
|
548
|
+
except Exception:
|
|
549
|
+
pass
|
|
550
|
+
|
|
551
|
+
return 0
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
if __name__ == "__main__":
|
|
555
|
+
sys.exit(main())
|