@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
package/package.json
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@m13v/s4l",
|
|
3
|
+
"version": "1.6.197-rc.7",
|
|
4
|
+
"description": "Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Install as a Claude Code agent skill.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"social-autoposter": "bin/cli.js",
|
|
7
|
+
"s4l": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/server.js",
|
|
11
|
+
"dev": "node --watch bin/server.js",
|
|
12
|
+
"release:mcpb": "bash scripts/release-mcpb.sh",
|
|
13
|
+
"test": "node test/platform.test.js && node test/launchd-golden.test.js && node test/systemd-golden.test.js && node mcp/scripts/test_onboarding_contract.mjs && node mcp/scripts/test_onboarding_ledger.mjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"!bin/server.js",
|
|
18
|
+
"!bin/auth.js",
|
|
19
|
+
"scripts/*.py",
|
|
20
|
+
"!scripts/db_direct.py",
|
|
21
|
+
"!scripts/backfill_real_clicks.py",
|
|
22
|
+
"!scripts/historical_engagement.py",
|
|
23
|
+
"!scripts/style_length_report.py",
|
|
24
|
+
"!scripts/install_lane_monitor.py",
|
|
25
|
+
"!scripts/li_discover_insert.py",
|
|
26
|
+
"!scripts/_dm_record_sent.sh",
|
|
27
|
+
"!scripts/send_batch_dms.sh",
|
|
28
|
+
"!scripts/mint_podlog_subpage_*.py",
|
|
29
|
+
"!scripts/tmp_*.py",
|
|
30
|
+
"!scripts/insert_post*.py",
|
|
31
|
+
"!scripts/_insert_post_*.py",
|
|
32
|
+
"!scripts/_log_cyrano_*.py",
|
|
33
|
+
"!scripts/_redditlink_finalize.py",
|
|
34
|
+
"!scripts/_seo_lane_roi.py",
|
|
35
|
+
"!scripts/check_improve_runs.py",
|
|
36
|
+
"!scripts/classify_all_dms.py",
|
|
37
|
+
"!scripts/migrate_*.py",
|
|
38
|
+
"!scripts/_gsc_roi_query.py",
|
|
39
|
+
"!scripts/batch_send_dms.py",
|
|
40
|
+
"!scripts/finalize_post_*.py",
|
|
41
|
+
"!scripts/insert_li_notifs.py",
|
|
42
|
+
"!scripts/li_notif_scan_process.py",
|
|
43
|
+
"!scripts/phase_d_edit.py",
|
|
44
|
+
"!scripts/phase_d_new_comments.py",
|
|
45
|
+
"!scripts/realign_sequences.py",
|
|
46
|
+
"!scripts/scratch_*.py",
|
|
47
|
+
"!scripts/seed_dashboard_users.py",
|
|
48
|
+
"!scripts/send_dashboard_invite.py",
|
|
49
|
+
"!scripts/_dm_icp_batch.py",
|
|
50
|
+
"!scripts/_li_discover_pending.py",
|
|
51
|
+
"!scripts/_phaseA_build_envelope.py",
|
|
52
|
+
"!scripts/_phaseA_select_5AI7GO.py",
|
|
53
|
+
"!scripts/_scan_aggregate.py",
|
|
54
|
+
"!scripts/_scan_timeline.py",
|
|
55
|
+
"!scripts/_serp_report.py",
|
|
56
|
+
"!scripts/_serp_vs_gsc_report.py",
|
|
57
|
+
"!scripts/_test_since_hook.py",
|
|
58
|
+
"!scripts/add_deploy_metadata.py",
|
|
59
|
+
"!scripts/add_nightowl_to_config.py",
|
|
60
|
+
"!scripts/amplitude_user_lookup.py",
|
|
61
|
+
"!scripts/audit_signup_wiring.py",
|
|
62
|
+
"!scripts/backfill_aggregate_stats.py",
|
|
63
|
+
"!scripts/backfill_claude_session_subagents.py",
|
|
64
|
+
"!scripts/backfill_crossroute_attribution.py",
|
|
65
|
+
"!scripts/backfill_ensure_dms.py",
|
|
66
|
+
"!scripts/backfill_icp_precheck.py",
|
|
67
|
+
"!scripts/backfill_linkedin_activity_urns.py",
|
|
68
|
+
"!scripts/backfill_mk0r_get_started.py",
|
|
69
|
+
"!scripts/backfill_run_monitor.py",
|
|
70
|
+
"!scripts/backfill_seo_authors.py",
|
|
71
|
+
"!scripts/backfill_seo_engagement.py",
|
|
72
|
+
"!scripts/backfill_target_project.py",
|
|
73
|
+
"!scripts/blog_refactor_single_route.py",
|
|
74
|
+
"!scripts/browser_watch.py",
|
|
75
|
+
"!scripts/check_analytics_wiring.py",
|
|
76
|
+
"!scripts/check_backfill_replied.py",
|
|
77
|
+
"!scripts/check_contrast.py",
|
|
78
|
+
"!scripts/check_deploy_wiring.py",
|
|
79
|
+
"!scripts/check_layout_wiring.py",
|
|
80
|
+
"!scripts/check_link_rules.py",
|
|
81
|
+
"!scripts/check_pep604_annotations.py",
|
|
82
|
+
"!scripts/cleanup_moltbook_dupes_16060.py",
|
|
83
|
+
"!scripts/cohort_score_distribution.py",
|
|
84
|
+
"!scripts/daily_stats_email.py",
|
|
85
|
+
"!scripts/delete_twitter_posts.py",
|
|
86
|
+
"!scripts/dm_helper.py",
|
|
87
|
+
"!scripts/export_cdp_storage_state.py",
|
|
88
|
+
"!scripts/export_kent_handoff.py",
|
|
89
|
+
"!scripts/extract_user_messages_today.py",
|
|
90
|
+
"!scripts/fazm_seo_health.py",
|
|
91
|
+
"!scripts/fix_mdx_light_mode.py",
|
|
92
|
+
"!scripts/fix_style_lengths_20260601.py",
|
|
93
|
+
"!scripts/fix_svg_paragraph_wrap.py",
|
|
94
|
+
"!scripts/ig_collate_transcripts.py",
|
|
95
|
+
"!scripts/ingest_human_seo_replies.py",
|
|
96
|
+
"!scripts/install_lane_digest.py",
|
|
97
|
+
"!scripts/li_process_notifications.py",
|
|
98
|
+
"!scripts/li_process_notifs.py",
|
|
99
|
+
"!scripts/octolens_threads.py",
|
|
100
|
+
"!scripts/octolens_twitter_batch.py",
|
|
101
|
+
"!scripts/octolens_twitter_cdp.py",
|
|
102
|
+
"!scripts/poll_web_chat.py",
|
|
103
|
+
"!scripts/process_li_notifs.py",
|
|
104
|
+
"!scripts/project_deploy_status.py",
|
|
105
|
+
"!scripts/regenerate_ig_plists.py",
|
|
106
|
+
"!scripts/send_comment_replies.py",
|
|
107
|
+
"!scripts/seo_health_all_projects.py",
|
|
108
|
+
"!scripts/snapshot_style_targets.py",
|
|
109
|
+
"!scripts/socialcrawl.py",
|
|
110
|
+
"!scripts/sweep_guide_chrome.py",
|
|
111
|
+
"!scripts/sync_web_chat_config.py",
|
|
112
|
+
"!scripts/test_own_reply_dedup.py",
|
|
113
|
+
"!scripts/twitter_compose_dm.py",
|
|
114
|
+
"scripts/*.sh",
|
|
115
|
+
"!scripts/_dm_icp_batch.sh",
|
|
116
|
+
"config.example.json",
|
|
117
|
+
"requirements.txt",
|
|
118
|
+
"SKILL.md",
|
|
119
|
+
"skill/*.sh",
|
|
120
|
+
"skill/lib/*.sh",
|
|
121
|
+
"setup/SKILL.md",
|
|
122
|
+
"browser-agent-configs/",
|
|
123
|
+
"mcp-servers/",
|
|
124
|
+
"mcp/dist/",
|
|
125
|
+
"!mcp/dist/pipeline.tgz",
|
|
126
|
+
"mcp/shared/",
|
|
127
|
+
"mcp/menubar/",
|
|
128
|
+
"mcp/package.json",
|
|
129
|
+
"mcp/manifest.json",
|
|
130
|
+
"mcp/install.mjs",
|
|
131
|
+
"mcp/install-runtime.mjs",
|
|
132
|
+
"!**/__pycache__/**",
|
|
133
|
+
"!**/*.pyc"
|
|
134
|
+
],
|
|
135
|
+
"keywords": [
|
|
136
|
+
"social-media",
|
|
137
|
+
"automation",
|
|
138
|
+
"claude",
|
|
139
|
+
"claude-code",
|
|
140
|
+
"ai-agent",
|
|
141
|
+
"reddit",
|
|
142
|
+
"twitter",
|
|
143
|
+
"linkedin"
|
|
144
|
+
],
|
|
145
|
+
"author": "Matthew Diakonov",
|
|
146
|
+
"license": "MIT",
|
|
147
|
+
"repository": {
|
|
148
|
+
"type": "git",
|
|
149
|
+
"url": "git+https://github.com/m13v/social-autoposter.git"
|
|
150
|
+
},
|
|
151
|
+
"homepage": "https://github.com/m13v/social-autoposter",
|
|
152
|
+
"engines": {
|
|
153
|
+
"node": ">=16"
|
|
154
|
+
},
|
|
155
|
+
"dependencies": {
|
|
156
|
+
"firebase-admin": "^13.8.0",
|
|
157
|
+
"pg": "^8.20.0",
|
|
158
|
+
"ws": "^8.0.0"
|
|
159
|
+
}
|
|
160
|
+
}
|
package/requirements.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# social-autoposter Python deps.
|
|
2
|
+
# Installed by bin/cli.js on init/update into the SAPS_PYTHON interpreter
|
|
3
|
+
# (Homebrew python), via `<saps_python> -m pip install -r requirements.txt`.
|
|
4
|
+
# Playwright browser binaries are downloaded separately (same interpreter:
|
|
5
|
+
# `<saps_python> -m playwright install chromium`).
|
|
6
|
+
|
|
7
|
+
playwright
|
|
8
|
+
# CDP client used by scripts/restore_twitter_session.py and the cookie-grab/
|
|
9
|
+
# CDP-driven helpers. Required on AppMaker VMs for the post-substitution
|
|
10
|
+
# auto-restore flow (bootstrap-vm, restore_twitter_session.py, CDP inject).
|
|
11
|
+
websocket-client
|
|
12
|
+
# AES-CBC decrypt of Chromium cookie values for scripts/copy_browser_cookies.py
|
|
13
|
+
# (the connect_x auto-import that copies the user's x.com session from their
|
|
14
|
+
# everyday browser into the autoposter's managed Chrome). Without this, the
|
|
15
|
+
# MCP setup connect_x step cannot auto-import and falls back to manual login.
|
|
16
|
+
cryptography
|
|
17
|
+
# Error reporting for the Python posting pipeline -> Sentry (org mediar-n5),
|
|
18
|
+
# initialized best-effort by scripts/sentry_init.py via scripts/http_api.py.
|
|
19
|
+
# Safe no-op when absent or when no DSN is configured.
|
|
20
|
+
sentry-sdk
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import os, re, json, subprocess, glob
|
|
2
|
+
REPO = os.path.expanduser("~/social-autoposter"); os.chdir(REPO)
|
|
3
|
+
all_py = {os.path.basename(p) for p in glob.glob("scripts/*.py")}
|
|
4
|
+
|
|
5
|
+
entry_surfaces = glob.glob("skill/*.sh") + glob.glob("skill/lib/*.sh")
|
|
6
|
+
entry_surfaces += ["SKILL.md", "setup/SKILL.md", "bin/cli.js"]
|
|
7
|
+
entry_surfaces += glob.glob("mcp/dist/*.js") + glob.glob("mcp/*.mjs")
|
|
8
|
+
|
|
9
|
+
ref_re = re.compile(r"scripts/([A-Za-z0-9_]+)\.py")
|
|
10
|
+
def refs_in_text(txt):
|
|
11
|
+
return {m+".py" for m in ref_re.findall(txt) if (m+".py") in all_py}
|
|
12
|
+
|
|
13
|
+
entries, surface_hits = set(), {}
|
|
14
|
+
for s in entry_surfaces:
|
|
15
|
+
if not os.path.exists(s): continue
|
|
16
|
+
txt = open(s, encoding="utf-8", errors="ignore").read()
|
|
17
|
+
# strip // comments for js, # comments for md is harder; keep simple: count all refs but mark cli.js comment lines
|
|
18
|
+
for fn in refs_in_text(txt):
|
|
19
|
+
entries.add(fn); surface_hits.setdefault(fn,set()).add(s)
|
|
20
|
+
|
|
21
|
+
imp_res = [re.compile(r"^\s*import\s+([A-Za-z0-9_]+)", re.M),
|
|
22
|
+
re.compile(r"^\s*from\s+([A-Za-z0-9_]+)\s+import", re.M),
|
|
23
|
+
re.compile(r"^\s*from\s+scripts\s+import\s+([A-Za-z0-9_,\s]+)", re.M),
|
|
24
|
+
re.compile(r"^\s*from\s+scripts\.([A-Za-z0-9_]+)\s+import", re.M),
|
|
25
|
+
re.compile(r"^\s*import\s+scripts\.([A-Za-z0-9_]+)", re.M)]
|
|
26
|
+
def expand(fn):
|
|
27
|
+
path=os.path.join("scripts",fn)
|
|
28
|
+
if not os.path.exists(path): return set()
|
|
29
|
+
txt=open(path,encoding="utf-8",errors="ignore").read()
|
|
30
|
+
found=set()
|
|
31
|
+
for rx in imp_res:
|
|
32
|
+
for g in rx.findall(txt):
|
|
33
|
+
for name in re.split(r"[,\s]+",g):
|
|
34
|
+
name=name.strip()
|
|
35
|
+
if name and (name+".py") in all_py: found.add(name+".py")
|
|
36
|
+
found |= refs_in_text(txt) # NEW: intra-python subprocess scripts/X.py refs
|
|
37
|
+
# follow symlink targets too (update_stats.py -> stats.py)
|
|
38
|
+
if os.path.islink(path):
|
|
39
|
+
tgt=os.path.basename(os.readlink(path))
|
|
40
|
+
if tgt in all_py: found.add(tgt)
|
|
41
|
+
return found
|
|
42
|
+
|
|
43
|
+
closure=set(entries); stack=list(entries)
|
|
44
|
+
while stack:
|
|
45
|
+
for d in expand(stack.pop()):
|
|
46
|
+
if d not in closure: closure.add(d); stack.append(d)
|
|
47
|
+
|
|
48
|
+
out=subprocess.run(["npm","pack","--dry-run","--json"],capture_output=True,text=True)
|
|
49
|
+
shipped=sorted(os.path.basename(f["path"]) for f in json.loads(out.stdout)[0]["files"] if f["path"].startswith("scripts/") and f["path"].endswith(".py"))
|
|
50
|
+
drop=sorted(set(shipped)-closure); keep=sorted(set(shipped)&closure)
|
|
51
|
+
print("entries:",len(entries),"| closure:",len(closure),"| shipped:",len(shipped))
|
|
52
|
+
print(f"\n=== KEEP (shipped & needed): {len(keep)}")
|
|
53
|
+
print(f"=== DROP (shipped but unreferenced anywhere consumer): {len(drop)}")
|
|
54
|
+
for d in drop:
|
|
55
|
+
if d=="_compute_allowlist.py": continue
|
|
56
|
+
print(" -",d)
|
|
57
|
+
open("/tmp/keep.txt","w").write("\n".join(k for k in keep if k!="_compute_allowlist.py"))
|
|
58
|
+
open("/tmp/drop.txt","w").write("\n".join(d for d in drop if d!="_compute_allowlist.py"))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Scratch driver: read JSON list of {post_id, session, text} from argv[1] and
|
|
3
|
+
run link_edit_helper mark-edited + dm_short_links backfill-post for each.
|
|
4
|
+
Gitignored scratch helper for the reddit link-edit run."""
|
|
5
|
+
import json, subprocess, sys, os
|
|
6
|
+
|
|
7
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
8
|
+
items = json.load(open(sys.argv[1]))
|
|
9
|
+
for it in items:
|
|
10
|
+
pid = str(it["post_id"]); sess = it["session"]; text = it["text"]
|
|
11
|
+
src = it.get("source", "plain_url_ab_skip")
|
|
12
|
+
r1 = subprocess.run([sys.executable, os.path.join(HERE, "link_edit_helper.py"),
|
|
13
|
+
"mark-edited", "--post-id", pid, "--content", text, "--source", src],
|
|
14
|
+
capture_output=True, text=True)
|
|
15
|
+
r2 = subprocess.run([sys.executable, os.path.join(HERE, "dm_short_links.py"),
|
|
16
|
+
"backfill-post", "--minted-session", sess, "--post-id", pid],
|
|
17
|
+
capture_output=True, text=True)
|
|
18
|
+
bf = (r2.stdout or "").strip().splitlines()[-1:] or [""]
|
|
19
|
+
print(f"post {pid}: mark_edited_rc={r1.returncode} backfill={bf[0]}"
|
|
20
|
+
+ (f" ERR1={r1.stderr.strip()}" if r1.returncode else ""))
|
package/scripts/_filt.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import sys,json
|
|
2
|
+
d=json.load(sys.stdin)
|
|
3
|
+
print("result_count:",d.get("result_count"),"error:",d.get("error"))
|
|
4
|
+
for r in d.get("results",[])[:5]:
|
|
5
|
+
name=r.get("author_name")
|
|
6
|
+
hl=(r.get("author_headline") or "")[:75]
|
|
7
|
+
t=(r.get("post_text") or "")[:170].replace("\n"," ")
|
|
8
|
+
print(" - %s | %s | age=%sh rx=%s c=%s vs=%s" % (name,hl,r.get("age_hours"),r.get("reactions"),r.get("comments"),r.get("velocity_score")))
|
|
9
|
+
print(" "+t)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import json, re
|
|
2
|
+
|
|
3
|
+
items = json.load(open("/tmp/li_actionable.json"))
|
|
4
|
+
cids = set(l.strip() for l in open("/tmp/li_cids.txt") if l.strip())
|
|
5
|
+
pairs = set(l.strip() for l in open("/tmp/li_pairs.txt") if l.strip())
|
|
6
|
+
posts = []
|
|
7
|
+
for l in open("/tmp/li_posts.txt"):
|
|
8
|
+
l=l.strip()
|
|
9
|
+
if not l: continue
|
|
10
|
+
pid, _, url = l.partition("|")
|
|
11
|
+
posts.append((pid, url))
|
|
12
|
+
|
|
13
|
+
EXCLUDED = {"louis030195","louis3195"}
|
|
14
|
+
OWN = {"matthew diakonov","m13v"}
|
|
15
|
+
|
|
16
|
+
def parse_urn(urn):
|
|
17
|
+
# urn:li:comment:(NS:PARENT,COMMENT)
|
|
18
|
+
m = re.match(r"urn:li:comment:\((\w+):(\d+),(\d+)\)", urn or "")
|
|
19
|
+
if not m: return (None,None,None)
|
|
20
|
+
return m.group(1), m.group(2), m.group(3)
|
|
21
|
+
|
|
22
|
+
def find_post_id(parent_id):
|
|
23
|
+
for pid, url in posts:
|
|
24
|
+
if parent_id and parent_id in url:
|
|
25
|
+
return pid
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
def author_engaged(author, parent_id):
|
|
29
|
+
# any engaged pair with same author AND url containing parent_id
|
|
30
|
+
al = author.strip().lower()
|
|
31
|
+
for p in pairs:
|
|
32
|
+
a, _, url = p.partition("|||")
|
|
33
|
+
if a.strip().lower()==al and parent_id and parent_id in url:
|
|
34
|
+
return True
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
seen_batch = set()
|
|
38
|
+
plan = []
|
|
39
|
+
counts = dict(new=0, already=0, engaged=0, excluded=0, own=0, nourn=0, dup_batch=0)
|
|
40
|
+
|
|
41
|
+
for it in items:
|
|
42
|
+
urn = it.get("comment_urn")
|
|
43
|
+
author = (it.get("author") or "").strip()
|
|
44
|
+
ns, parent_id, comment_id = parse_urn(urn)
|
|
45
|
+
rec = dict(it, ns=ns, parent_id=parent_id, comment_id=comment_id, decision=None, post_id=None)
|
|
46
|
+
|
|
47
|
+
if not urn or not parent_id:
|
|
48
|
+
rec["decision"]="skip:no_comment_urn"; counts["nourn"]+=1; plan.append(rec); continue
|
|
49
|
+
if urn in seen_batch:
|
|
50
|
+
rec["decision"]="skip:dup_in_batch"; counts["dup_batch"]+=1; plan.append(rec); continue
|
|
51
|
+
seen_batch.add(urn)
|
|
52
|
+
if urn in cids:
|
|
53
|
+
rec["decision"]="skip:already_tracked"; counts["already"]+=1; plan.append(rec); continue
|
|
54
|
+
al = author.lower()
|
|
55
|
+
if al in OWN or any(al==e for e in EXCLUDED):
|
|
56
|
+
rec["decision"]="skip:own_or_excluded"
|
|
57
|
+
if al in OWN: counts["own"]+=1
|
|
58
|
+
else: counts["excluded"]+=1
|
|
59
|
+
plan.append(rec); continue
|
|
60
|
+
if author_engaged(author, parent_id):
|
|
61
|
+
rec["decision"]="skip:author_already_engaged"; counts["engaged"]+=1; plan.append(rec); continue
|
|
62
|
+
pid = find_post_id(parent_id)
|
|
63
|
+
rec["post_id"]=pid
|
|
64
|
+
rec["decision"]="insert" if pid else "create_post+insert"
|
|
65
|
+
counts["new"]+=1
|
|
66
|
+
plan.append(rec)
|
|
67
|
+
|
|
68
|
+
json.dump(plan, open("/tmp/li_plan.json","w"), indent=2)
|
|
69
|
+
print("COUNTS:", counts)
|
|
70
|
+
print("TOTAL inspected:", len(items))
|
|
71
|
+
print()
|
|
72
|
+
for r in plan:
|
|
73
|
+
if r["decision"].startswith("skip"): continue
|
|
74
|
+
print(f"[{r['decision']}] {r['author']} | ns={r['ns']} parent={r['parent_id']} post_id={r['post_id']}")
|
|
75
|
+
print(f" urn: {r['comment_urn']}")
|
|
76
|
+
print(f" snip: {r['snippet'][:120]}")
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json, re, subprocess, os, sys
|
|
3
|
+
|
|
4
|
+
REPO = os.path.expanduser("~/social-autoposter")
|
|
5
|
+
LID = os.path.join(REPO, "scripts", "li_discovery.py")
|
|
6
|
+
|
|
7
|
+
EXCLUDED_AUTHORS = {"louis030195", "louis3195"}
|
|
8
|
+
OWN = {"matthew diakonov", "m13v"}
|
|
9
|
+
|
|
10
|
+
def run(args):
|
|
11
|
+
r = subprocess.run([sys.executable, LID] + args, capture_output=True, text=True)
|
|
12
|
+
return (r.stdout or "").strip(), (r.stderr or "").strip(), r.returncode
|
|
13
|
+
|
|
14
|
+
# one context dump
|
|
15
|
+
ctx_out, ctx_err, rc = run(["context"])
|
|
16
|
+
ctx = json.loads(ctx_out) if ctx_out else {}
|
|
17
|
+
existing = set(ctx.get("existing_comment_ids") or [])
|
|
18
|
+
engaged_pairs = ctx.get("engaged_pairs") or []
|
|
19
|
+
posts = ctx.get("posts") or []
|
|
20
|
+
|
|
21
|
+
PARENT_RE = re.compile(r"urn:li:(?:activity|ugcPost|share):(\d+)")
|
|
22
|
+
|
|
23
|
+
# build parent_id -> post_id map
|
|
24
|
+
parent_to_post = {}
|
|
25
|
+
for p in posts:
|
|
26
|
+
u = p.get("our_url") or ""
|
|
27
|
+
m = PARENT_RE.search(u)
|
|
28
|
+
if m:
|
|
29
|
+
parent_to_post.setdefault(m.group(1), p["id"])
|
|
30
|
+
|
|
31
|
+
# build engaged set (author_lower, parent_id)
|
|
32
|
+
engaged_set = set()
|
|
33
|
+
for pair in engaged_pairs:
|
|
34
|
+
if "|||" not in pair:
|
|
35
|
+
continue
|
|
36
|
+
author, url = pair.split("|||", 1)
|
|
37
|
+
m = PARENT_RE.search(url)
|
|
38
|
+
if m:
|
|
39
|
+
engaged_set.add((author.strip().lower(), m.group(1)))
|
|
40
|
+
|
|
41
|
+
CU_RE = re.compile(r"\((?:activity|ugcPost|share):(\d+),(\d+)\)")
|
|
42
|
+
|
|
43
|
+
data = json.load(open("/tmp/li_notifs.json"))
|
|
44
|
+
|
|
45
|
+
counts = dict(scanned=len(data), new=0, already=0, engaged=0, excluded=0, own=0, no_urn=0)
|
|
46
|
+
new_items = []
|
|
47
|
+
|
|
48
|
+
def proj_for(snippet):
|
|
49
|
+
s = (snippet or "").lower()
|
|
50
|
+
# our niche is claude code / ai agents -> fazm flagship
|
|
51
|
+
if any(k in s for k in ["claude code", "claude.md", "agent", "context window", "mcp", "subagent", "harness", "codex", "anthropic", "llm", "ai "]):
|
|
52
|
+
return "fazm"
|
|
53
|
+
return "general"
|
|
54
|
+
|
|
55
|
+
for it in data:
|
|
56
|
+
cu = it.get("comment_urn")
|
|
57
|
+
author = (it.get("author") or "").strip()
|
|
58
|
+
if not cu:
|
|
59
|
+
counts["no_urn"] += 1
|
|
60
|
+
continue
|
|
61
|
+
m = CU_RE.search(cu)
|
|
62
|
+
if not m:
|
|
63
|
+
counts["no_urn"] += 1
|
|
64
|
+
continue
|
|
65
|
+
parent_id = m.group(1)
|
|
66
|
+
al = author.lower()
|
|
67
|
+
# exclusion
|
|
68
|
+
if al in OWN or author in ("unknown",):
|
|
69
|
+
counts["own"] += 1
|
|
70
|
+
continue
|
|
71
|
+
if al in EXCLUDED_AUTHORS or any(x in al for x in EXCLUDED_AUTHORS):
|
|
72
|
+
counts["excluded"] += 1
|
|
73
|
+
continue
|
|
74
|
+
if cu in existing:
|
|
75
|
+
counts["already"] += 1
|
|
76
|
+
continue
|
|
77
|
+
if (al, parent_id) in engaged_set:
|
|
78
|
+
counts["engaged"] += 1
|
|
79
|
+
continue
|
|
80
|
+
# find or create post
|
|
81
|
+
post_id = parent_to_post.get(parent_id)
|
|
82
|
+
if not post_id:
|
|
83
|
+
proj = proj_for(it.get("snippet"))
|
|
84
|
+
out, err, rc = run(["create-post", "--activity-id", parent_id, "--project", proj, "--author", author])
|
|
85
|
+
post_id = out.strip().splitlines()[-1] if out.strip() else ""
|
|
86
|
+
if not post_id:
|
|
87
|
+
print(f" [create-post FAILED parent={parent_id}] err={err}", file=sys.stderr)
|
|
88
|
+
continue
|
|
89
|
+
parent_to_post[parent_id] = post_id
|
|
90
|
+
# insert reply
|
|
91
|
+
out, err, rc = run([
|
|
92
|
+
"insert-reply", "--post-id", str(post_id),
|
|
93
|
+
"--comment-urn", cu, "--author", author,
|
|
94
|
+
"--content", (it.get("snippet") or "")[:3000],
|
|
95
|
+
"--href", it.get("href") or "",
|
|
96
|
+
])
|
|
97
|
+
res = out.strip().splitlines()[-1] if out.strip() else ""
|
|
98
|
+
if res == "duplicate":
|
|
99
|
+
counts["already"] += 1
|
|
100
|
+
elif res.startswith("gated"):
|
|
101
|
+
counts["engaged"] += 1 # gated by blocklist/velocity, not actionable
|
|
102
|
+
print(f" [gated] {author} parent={parent_id} -> {res}", file=sys.stderr)
|
|
103
|
+
elif res:
|
|
104
|
+
counts["new"] += 1
|
|
105
|
+
new_items.append((res, author, parent_id, cu))
|
|
106
|
+
# mark as existing to dedup within this run
|
|
107
|
+
existing.add(cu)
|
|
108
|
+
engaged_set.add((al, parent_id))
|
|
109
|
+
else:
|
|
110
|
+
print(f" [insert FAILED] {author} parent={parent_id} err={err}", file=sys.stderr)
|
|
111
|
+
|
|
112
|
+
print("\n=== NEW REPLIES INSERTED ===")
|
|
113
|
+
for rid, author, parent, cu in new_items:
|
|
114
|
+
print(f" reply_id={rid} author={author} parent={parent}")
|
|
115
|
+
|
|
116
|
+
print("\n=== SUMMARY ===")
|
|
117
|
+
print(f"New replies discovered: {counts['new']}")
|
|
118
|
+
print(f"Already tracked: {counts['already']}")
|
|
119
|
+
print(f"Author already engaged thread: {counts['engaged']}")
|
|
120
|
+
print(f"Excluded: {counts['excluded']}")
|
|
121
|
+
print(f"Own account: {counts['own']}")
|
|
122
|
+
print(f"No comment URN: {counts['no_urn']}")
|
|
123
|
+
print(f"Total scanned: {counts['scanned']}")
|
|
124
|
+
|
|
125
|
+
excl_total = counts["excluded"] + counts["own"]
|
|
126
|
+
print(f"\nLINKEDIN_SCAN_SUMMARY: scanned={counts['scanned']} new={counts['new']} already={counts['already']} excluded={excl_total} unmatched={counts['no_urn']}")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import json, os, sys, time, subprocess, signal
|
|
2
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
|
|
3
|
+
|
|
4
|
+
LOCK = os.path.expanduser("~/.claude/twitter-browser-lock.json")
|
|
5
|
+
os.makedirs(os.path.dirname(LOCK), exist_ok=True)
|
|
6
|
+
|
|
7
|
+
def write_lock(pid, role):
|
|
8
|
+
with open(LOCK, "w") as f:
|
|
9
|
+
json.dump({"session_id": f"python:{pid}", "timestamp": int(time.time()), "role": role}, f)
|
|
10
|
+
|
|
11
|
+
def clean():
|
|
12
|
+
try: os.remove(LOCK)
|
|
13
|
+
except OSError: pass
|
|
14
|
+
|
|
15
|
+
# ---- TEST 1: poster preempts a LIVE scan holder ----
|
|
16
|
+
clean()
|
|
17
|
+
victim = subprocess.Popen(["sleep", "120"])
|
|
18
|
+
write_lock(victim.pid, "scan")
|
|
19
|
+
os.environ["S4L_LOCK_ROLE"] = "post"
|
|
20
|
+
import importlib, twitter_browser
|
|
21
|
+
importlib.reload(twitter_browser)
|
|
22
|
+
t0 = time.time()
|
|
23
|
+
twitter_browser._acquire_browser_lock()
|
|
24
|
+
dt = time.time() - t0
|
|
25
|
+
held = json.load(open(LOCK))
|
|
26
|
+
victim_alive = victim.poll() is None
|
|
27
|
+
print(f"TEST1 poster-preempts-scan: took={dt:.1f}s holder_now={held['session_id']} role={held.get('role')} victim_alive={victim_alive}")
|
|
28
|
+
assert held["session_id"] == f"python:{os.getpid()}", "poster should hold lock"
|
|
29
|
+
assert held.get("role") == "post"
|
|
30
|
+
assert not victim_alive, "scan victim should be killed"
|
|
31
|
+
assert dt < 10, "should preempt fast, not wait 45s"
|
|
32
|
+
if victim.poll() is None: victim.kill()
|
|
33
|
+
print("TEST1 PASS")
|
|
34
|
+
|
|
35
|
+
# ---- TEST 2: scanner does NOT preempt a live POST holder (waits then gives up) ----
|
|
36
|
+
clean()
|
|
37
|
+
poster = subprocess.Popen(["sleep", "120"])
|
|
38
|
+
write_lock(poster.pid, "post")
|
|
39
|
+
os.environ["S4L_LOCK_ROLE"] = "scan"
|
|
40
|
+
twitter_browser.LOCK_WAIT_MAX = 4 # shorten the give-up window for the test
|
|
41
|
+
twitter_browser.LOCK_ROLE = "scan"
|
|
42
|
+
twitter_browser._LOCK_SESSION_ID = f"python:{os.getpid()}"
|
|
43
|
+
twitter_browser._LOCK_INHERITED = False
|
|
44
|
+
import io, contextlib
|
|
45
|
+
t0 = time.time()
|
|
46
|
+
rc = None
|
|
47
|
+
try:
|
|
48
|
+
twitter_browser._acquire_browser_lock()
|
|
49
|
+
rc = "acquired"
|
|
50
|
+
except SystemExit as e:
|
|
51
|
+
rc = f"exit:{e.code}"
|
|
52
|
+
dt = time.time() - t0
|
|
53
|
+
poster_alive = poster.poll() is None
|
|
54
|
+
print(f"TEST2 scanner-vs-post: result={rc} took={dt:.1f}s poster_alive={poster_alive}")
|
|
55
|
+
assert rc == "exit:1", "scanner must NOT take a live post holder; should give up"
|
|
56
|
+
assert poster_alive, "scanner must NOT kill the poster"
|
|
57
|
+
if poster.poll() is None: poster.kill()
|
|
58
|
+
print("TEST2 PASS")
|
|
59
|
+
clean()
|
|
60
|
+
print("ALL PASS")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Throwaway helper: run dm_conversation.py set-icp-precheck for every project.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 _run_icp_precheck.py --dm-id 4583 --default-label icp_miss \
|
|
6
|
+
--default-notes "crypto researcher, not target vertical" \
|
|
7
|
+
--override 'fazm=icp_miss=engaged on agentic token cost but no claude-code-wrapper signal' \
|
|
8
|
+
--override 'Agora=icp_miss=crypto researcher not a protocol governance buyer'
|
|
9
|
+
|
|
10
|
+
Any project NOT overridden gets the default label/notes.
|
|
11
|
+
"""
|
|
12
|
+
import argparse, subprocess, sys
|
|
13
|
+
|
|
14
|
+
PROJECTS = [
|
|
15
|
+
"fazm", "Terminator", "macOS MCP", "Vipassana", "S4L", "AI Browser Profile",
|
|
16
|
+
"WhatsApp MCP", "macOS Session Replay", "Cyrano", "Assrt", "PieLine", "Clone",
|
|
17
|
+
"mk0r", "fde10x", "claude-meter", "c0nsl", "tenxats", "paperback-expert",
|
|
18
|
+
"studyly", "Mediar", "NightOwl", "Runner", "Agora", "Podlog", "ccmd",
|
|
19
|
+
]
|
|
20
|
+
SCRIPT = "/Users/matthewdi/social-autoposter/scripts/dm_conversation.py"
|
|
21
|
+
|
|
22
|
+
def main():
|
|
23
|
+
ap = argparse.ArgumentParser()
|
|
24
|
+
ap.add_argument("--dm-id", required=True)
|
|
25
|
+
ap.add_argument("--default-label", default="icp_miss")
|
|
26
|
+
ap.add_argument("--default-notes", default="not target vertical")
|
|
27
|
+
ap.add_argument("--override", action="append", default=[],
|
|
28
|
+
help="PROJECT=LABEL=NOTES")
|
|
29
|
+
args = ap.parse_args()
|
|
30
|
+
|
|
31
|
+
overrides = {}
|
|
32
|
+
for o in args.override:
|
|
33
|
+
parts = o.split("=", 2)
|
|
34
|
+
if len(parts) != 3:
|
|
35
|
+
print("bad override:", o); sys.exit(2)
|
|
36
|
+
overrides[parts[0]] = (parts[1], parts[2])
|
|
37
|
+
|
|
38
|
+
# validate override keys
|
|
39
|
+
for k in overrides:
|
|
40
|
+
if k not in PROJECTS:
|
|
41
|
+
print("UNKNOWN project in override:", k); sys.exit(2)
|
|
42
|
+
|
|
43
|
+
fails = 0
|
|
44
|
+
for p in PROJECTS:
|
|
45
|
+
label, notes = overrides.get(p, (args.default_label, args.default_notes))
|
|
46
|
+
r = subprocess.run(
|
|
47
|
+
["python3", SCRIPT, "set-icp-precheck", "--dm-id", args.dm_id,
|
|
48
|
+
"--project", p, "--label", label, "--notes", notes],
|
|
49
|
+
capture_output=True, text=True)
|
|
50
|
+
tag = "ok" if r.returncode == 0 else "FAIL"
|
|
51
|
+
if r.returncode != 0:
|
|
52
|
+
fails += 1
|
|
53
|
+
print(f"{tag} {p}={label}", (r.stderr or r.stdout or "").strip()[:120])
|
|
54
|
+
print(f"DONE dm={args.dm_id} fails={fails}")
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
main()
|