@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.
Files changed (326) hide show
  1. package/README.md +143 -0
  2. package/SKILL.md +342 -0
  3. package/bin/cli.js +980 -0
  4. package/bin/cookie-helper.js +315 -0
  5. package/bin/platform.js +59 -0
  6. package/bin/scheduler/index.js +12 -0
  7. package/bin/scheduler/launchd.js +518 -0
  8. package/browser-agent-configs/all-agents-mcp.json +68 -0
  9. package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
  10. package/browser-agent-configs/linkedin-agent.json +17 -0
  11. package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
  12. package/browser-agent-configs/reddit-agent-mcp.json +16 -0
  13. package/browser-agent-configs/reddit-agent.json +17 -0
  14. package/browser-agent-configs/twitter-harness-mcp.json +18 -0
  15. package/config.example.json +45 -0
  16. package/mcp/dist/index.js +4212 -0
  17. package/mcp/dist/onboarding.js +200 -0
  18. package/mcp/dist/panel.html +176 -0
  19. package/mcp/dist/product-link.html +102 -0
  20. package/mcp/dist/repo.js +222 -0
  21. package/mcp/dist/runtime.js +1079 -0
  22. package/mcp/dist/screencast.js +323 -0
  23. package/mcp/dist/setup.js +545 -0
  24. package/mcp/dist/telemetry.js +306 -0
  25. package/mcp/dist/twitterAuth.js +138 -0
  26. package/mcp/dist/version.js +271 -0
  27. package/mcp/dist/version.json +4 -0
  28. package/mcp/install-runtime.mjs +70 -0
  29. package/mcp/install.mjs +169 -0
  30. package/mcp/manifest.json +80 -0
  31. package/mcp/menubar/dashboard_server.py +213 -0
  32. package/mcp/menubar/s4l_card.py +1336 -0
  33. package/mcp/menubar/s4l_log_relay.py +179 -0
  34. package/mcp/menubar/s4l_menubar.py +2439 -0
  35. package/mcp/menubar/s4l_state.py +891 -0
  36. package/mcp/package.json +34 -0
  37. package/mcp/shared/doctor.cjs +437 -0
  38. package/mcp/shared/onboarding-ledger.cjs +324 -0
  39. package/mcp-servers/browser-harness/server.py +968 -0
  40. package/package.json +160 -0
  41. package/requirements.txt +20 -0
  42. package/scripts/_compute_allowlist.py +58 -0
  43. package/scripts/_db_update.py +20 -0
  44. package/scripts/_filt.py +9 -0
  45. package/scripts/_li_notif_match.py +76 -0
  46. package/scripts/_li_notif_orchestrate.py +126 -0
  47. package/scripts/_lock_preempt_test.py +60 -0
  48. package/scripts/_run_icp_precheck.py +57 -0
  49. package/scripts/a16z_pearx_calendar_reminders.py +99 -0
  50. package/scripts/account_resolver.py +141 -0
  51. package/scripts/active_campaigns.py +114 -0
  52. package/scripts/active_users.py +190 -0
  53. package/scripts/amplitude_24h_signups.py +468 -0
  54. package/scripts/amplitude_signups.py +177 -0
  55. package/scripts/apply_onboarding_selections.py +131 -0
  56. package/scripts/audience_pages.py +243 -0
  57. package/scripts/audit_helper.py +120 -0
  58. package/scripts/author_history_block.py +353 -0
  59. package/scripts/autopilot_stall_watch.py +284 -0
  60. package/scripts/backfill_twitter_attempts_topic.py +81 -0
  61. package/scripts/backfill_twitter_log_post_no_id.py +322 -0
  62. package/scripts/bench_dashboard.sh +138 -0
  63. package/scripts/bh_send.py +39 -0
  64. package/scripts/build_persona.py +409 -0
  65. package/scripts/bulk_icp.py +18 -0
  66. package/scripts/campaign_bump.py +51 -0
  67. package/scripts/capture_thread_media.py +288 -0
  68. package/scripts/check_browser_lock_health.sh +81 -0
  69. package/scripts/check_external_pool_depth.py +253 -0
  70. package/scripts/check_unread_web_chats.py +28 -0
  71. package/scripts/claim_web_chat.py +47 -0
  72. package/scripts/classify_run_error.py +158 -0
  73. package/scripts/claude_job.py +988 -0
  74. package/scripts/clean_stale_singleton.sh +56 -0
  75. package/scripts/cleanup_harness_tabs.py +68 -0
  76. package/scripts/copy_browser_cookies.py +454 -0
  77. package/scripts/counterparty_history.py +350 -0
  78. package/scripts/db.py +57 -0
  79. package/scripts/discover_claude_profiles.py +120 -0
  80. package/scripts/discover_linkedin_candidates.py +984 -0
  81. package/scripts/dm_conversation.py +682 -0
  82. package/scripts/dm_db_update.py +69 -0
  83. package/scripts/dm_engage_helper.py +161 -0
  84. package/scripts/dm_outreach_helper.py +147 -0
  85. package/scripts/dm_outreach_twitter_helper.py +129 -0
  86. package/scripts/dm_send_log.py +106 -0
  87. package/scripts/dm_short_links.py +1084 -0
  88. package/scripts/dump_web_chat_history.py +47 -0
  89. package/scripts/engage_github.py +640 -0
  90. package/scripts/engage_reddit.py +1235 -0
  91. package/scripts/engage_twitter_helper.py +301 -0
  92. package/scripts/engagement_styles.py +1787 -0
  93. package/scripts/enrich_twitter_candidates.py +82 -0
  94. package/scripts/feedback_digest.py +448 -0
  95. package/scripts/fetch_prospect_profile.py +312 -0
  96. package/scripts/fetch_twitter_t1.py +134 -0
  97. package/scripts/find_threads.py +530 -0
  98. package/scripts/follow_gate_log.py +59 -0
  99. package/scripts/funnel_per_day.py +194 -0
  100. package/scripts/generate_daily_human_style.py +494 -0
  101. package/scripts/generation_trace.py +173 -0
  102. package/scripts/get_run_cost.py +107 -0
  103. package/scripts/github_engage_helper.py +93 -0
  104. package/scripts/github_tools.py +509 -0
  105. package/scripts/harness_overlay.py +556 -0
  106. package/scripts/harvest_twitter_following.py +243 -0
  107. package/scripts/heartbeat.sh +70 -0
  108. package/scripts/history_context.py +284 -0
  109. package/scripts/http_api.py +206 -0
  110. package/scripts/human_dm_replies_helper.py +169 -0
  111. package/scripts/identity.py +302 -0
  112. package/scripts/ig_batch_creator.sh +93 -0
  113. package/scripts/ig_post_type_picker.py +243 -0
  114. package/scripts/ig_scrape_transcribe.sh +91 -0
  115. package/scripts/ingest_human_dm_replies.py +271 -0
  116. package/scripts/ingest_web_chat_replies.py +229 -0
  117. package/scripts/install_fleet.py +187 -0
  118. package/scripts/invent_mcp_server.py +350 -0
  119. package/scripts/invent_topics.py +1462 -0
  120. package/scripts/learned_preferences.py +263 -0
  121. package/scripts/li_discovery.py +161 -0
  122. package/scripts/link_edit_helper.py +142 -0
  123. package/scripts/link_tail.py +592 -0
  124. package/scripts/linkedin_api.py +561 -0
  125. package/scripts/linkedin_browser.py +730 -0
  126. package/scripts/linkedin_cooldown.py +128 -0
  127. package/scripts/linkedin_exclusions.py +234 -0
  128. package/scripts/linkedin_killswitch.py +1333 -0
  129. package/scripts/linkedin_search_topic_schema.py +49 -0
  130. package/scripts/linkedin_unipile.py +658 -0
  131. package/scripts/linkedin_url.py +228 -0
  132. package/scripts/log_claude_session.py +636 -0
  133. package/scripts/log_draft.py +143 -0
  134. package/scripts/log_linkedin_search_attempts.py +126 -0
  135. package/scripts/log_post.py +651 -0
  136. package/scripts/log_run.py +364 -0
  137. package/scripts/log_thread_media.py +108 -0
  138. package/scripts/log_twitter_search_attempts.py +150 -0
  139. package/scripts/log_twitter_skips.py +211 -0
  140. package/scripts/lookup_post.py +78 -0
  141. package/scripts/mark_web_chat_processed.py +32 -0
  142. package/scripts/mcp_lock_proxy.py +370 -0
  143. package/scripts/memory_snapshot.py +972 -0
  144. package/scripts/merge_review_queue.py +215 -0
  145. package/scripts/mint_external_pool.py +182 -0
  146. package/scripts/mint_kent_pool.py +249 -0
  147. package/scripts/moltbook_post.py +320 -0
  148. package/scripts/moltbook_tools.py +159 -0
  149. package/scripts/pending_threads.py +188 -0
  150. package/scripts/pick_ig_account.py +177 -0
  151. package/scripts/pick_project.py +208 -0
  152. package/scripts/pick_search_topic.py +771 -0
  153. package/scripts/pick_thread_target.py +279 -0
  154. package/scripts/pick_twitter_thread_target.py +202 -0
  155. package/scripts/podlog_fetch_batch.sh +32 -0
  156. package/scripts/post_github.py +1311 -0
  157. package/scripts/post_reddit.py +2668 -0
  158. package/scripts/precompute_dashboard_stats.py +204 -0
  159. package/scripts/preflight.sh +297 -0
  160. package/scripts/progress.py +88 -0
  161. package/scripts/project_excludes.py +353 -0
  162. package/scripts/project_slugs.py +91 -0
  163. package/scripts/project_stats.py +241 -0
  164. package/scripts/project_stats_json.py +1563 -0
  165. package/scripts/project_topics.py +192 -0
  166. package/scripts/qualified_query_bank.py +436 -0
  167. package/scripts/reap_stale_claude_sessions.py +867 -0
  168. package/scripts/reddit_browser.py +2549 -0
  169. package/scripts/reddit_browser_fetch.py +141 -0
  170. package/scripts/reddit_browser_lock.py +593 -0
  171. package/scripts/reddit_chat_sync.py +710 -0
  172. package/scripts/reddit_query_bank.py +200 -0
  173. package/scripts/reddit_threads_helper.py +151 -0
  174. package/scripts/reddit_tools.py +956 -0
  175. package/scripts/refresh_instagram_tokens.py +280 -0
  176. package/scripts/release-mcpb.sh +513 -0
  177. package/scripts/reply_db.py +334 -0
  178. package/scripts/reply_insert.py +98 -0
  179. package/scripts/reply_risk_digest.py +761 -0
  180. package/scripts/reset-test-machine.sh +602 -0
  181. package/scripts/restore_twitter_session.py +177 -0
  182. package/scripts/ripen_reddit_plan.py +478 -0
  183. package/scripts/run_claude.sh +433 -0
  184. package/scripts/run_moltbook_cycle.py +555 -0
  185. package/scripts/s4l_box_update.sh +226 -0
  186. package/scripts/s4l_channel.py +103 -0
  187. package/scripts/s4l_ctl.sh +75 -0
  188. package/scripts/s4l_env.py +47 -0
  189. package/scripts/saps_activity.py +126 -0
  190. package/scripts/saps_mode.py +328 -0
  191. package/scripts/scan_dm_candidates.py +580 -0
  192. package/scripts/scan_github_replies.py +168 -0
  193. package/scripts/scan_instagram_comments.py +481 -0
  194. package/scripts/scan_moltbook_replies.py +252 -0
  195. package/scripts/scan_pii.py +190 -0
  196. package/scripts/scan_reddit_replies.py +377 -0
  197. package/scripts/scan_twitter_mentions_browser.py +327 -0
  198. package/scripts/scan_twitter_thread_followups.py +299 -0
  199. package/scripts/scan_x_profile.py +384 -0
  200. package/scripts/schedule_state.py +202 -0
  201. package/scripts/scheduled_tasks_snapshot.py +123 -0
  202. package/scripts/score_linkedin_candidates.py +419 -0
  203. package/scripts/score_twitter_candidates.py +718 -0
  204. package/scripts/scrape_linkedin_comment_stats.py +1755 -0
  205. package/scripts/scrape_linkedin_stats_browser.py +52 -0
  206. package/scripts/scrape_reddit_views.py +365 -0
  207. package/scripts/seed_search_queries.py +453 -0
  208. package/scripts/seed_search_topics.py +127 -0
  209. package/scripts/send_web_chat_reply.py +130 -0
  210. package/scripts/sentry_init.py +128 -0
  211. package/scripts/setup_twitter_auth.py +1320 -0
  212. package/scripts/snapshot.py +583 -0
  213. package/scripts/stats.py +2702 -0
  214. package/scripts/stats_helper.py +52 -0
  215. package/scripts/strike_alert.py +783 -0
  216. package/scripts/sweep_post_link_clicks.py +107 -0
  217. package/scripts/sync_ig_to_posts.py +147 -0
  218. package/scripts/test_browser_lock.py +189 -0
  219. package/scripts/test_installation_api.sh +52 -0
  220. package/scripts/test_percard_posting.py +142 -0
  221. package/scripts/top_dud_linkedin_queries.py +71 -0
  222. package/scripts/top_dud_reddit_queries.py +67 -0
  223. package/scripts/top_dud_twitter_queries.py +71 -0
  224. package/scripts/top_dud_twitter_topics.py +102 -0
  225. package/scripts/top_linkedin_queries.py +55 -0
  226. package/scripts/top_omitted_reddit_topics.py +91 -0
  227. package/scripts/top_performers.py +588 -0
  228. package/scripts/top_search_topics.py +180 -0
  229. package/scripts/top_twitter_queries.py +190 -0
  230. package/scripts/twitter_access_check.py +382 -0
  231. package/scripts/twitter_account.py +41 -0
  232. package/scripts/twitter_batch_phase.py +126 -0
  233. package/scripts/twitter_browser.py +2804 -0
  234. package/scripts/twitter_cookie_mirror.py +130 -0
  235. package/scripts/twitter_cycle_helper.py +310 -0
  236. package/scripts/twitter_gen_links.py +287 -0
  237. package/scripts/twitter_post_plan.py +1188 -0
  238. package/scripts/twitter_scan.py +324 -0
  239. package/scripts/twitter_supply_signal.py +57 -0
  240. package/scripts/twitter_threads_helper.py +152 -0
  241. package/scripts/unclaim_web_chat.py +29 -0
  242. package/scripts/update_instagram_stats.py +261 -0
  243. package/scripts/update_linkedin_stats_from_feed.py +328 -0
  244. package/scripts/version.py +72 -0
  245. package/scripts/watchdog_hung_runs.py +343 -0
  246. package/scripts/write_generation_trace.py +73 -0
  247. package/setup/SKILL.md +277 -0
  248. package/skill/amplitude-24h-signups.sh +38 -0
  249. package/skill/archive-old-logs.sh +40 -0
  250. package/skill/audit-dm-staleness.sh +42 -0
  251. package/skill/audit-linkedin.sh +14 -0
  252. package/skill/audit-moltbook.sh +4 -0
  253. package/skill/audit-reddit-resurrect.sh +67 -0
  254. package/skill/audit-reddit.sh +4 -0
  255. package/skill/audit-twitter.sh +4 -0
  256. package/skill/audit.sh +287 -0
  257. package/skill/backfill-twitter-attempts-topic.sh +19 -0
  258. package/skill/backfill-twitter-ghost-posts.sh +24 -0
  259. package/skill/check-external-pool-depth.sh +7 -0
  260. package/skill/check-web-chats.sh +203 -0
  261. package/skill/dm-outreach-linkedin.sh +250 -0
  262. package/skill/dm-outreach-reddit.sh +274 -0
  263. package/skill/dm-outreach-twitter.sh +265 -0
  264. package/skill/engage-dm-replies-linkedin.sh +4 -0
  265. package/skill/engage-dm-replies-reddit.sh +4 -0
  266. package/skill/engage-dm-replies-twitter.sh +4 -0
  267. package/skill/engage-dm-replies.sh +1597 -0
  268. package/skill/engage-linkedin.sh +581 -0
  269. package/skill/engage-moltbook.sh +36 -0
  270. package/skill/engage-reddit.sh +146 -0
  271. package/skill/engage-twitter.sh +467 -0
  272. package/skill/github-engage.sh +176 -0
  273. package/skill/ingest-web-chat-replies.sh +38 -0
  274. package/skill/invent-supply-test.sh +100 -0
  275. package/skill/invent-topics.sh +50 -0
  276. package/skill/lib/linkedin-backend.sh +364 -0
  277. package/skill/lib/platform.sh +48 -0
  278. package/skill/lib/reddit-backend.sh +234 -0
  279. package/skill/lib/twitter-backend.sh +314 -0
  280. package/skill/link-edit-github.sh +136 -0
  281. package/skill/link-edit-moltbook.sh +117 -0
  282. package/skill/link-edit-reddit.sh +201 -0
  283. package/skill/linkedin-presence.sh +182 -0
  284. package/skill/linkedin-recovery.sh +282 -0
  285. package/skill/lock.sh +647 -0
  286. package/skill/memory-snapshot.sh +39 -0
  287. package/skill/precompute-stats.sh +35 -0
  288. package/skill/prewarm-funnel.sh +104 -0
  289. package/skill/refresh-instagram-tokens.sh +57 -0
  290. package/skill/refresh-twitter-following.sh +52 -0
  291. package/skill/reply-risk-digest.sh +31 -0
  292. package/skill/run-cycle-update-guard.sh +44 -0
  293. package/skill/run-draft-and-publish.sh +123 -0
  294. package/skill/run-generate-daily-style.sh +50 -0
  295. package/skill/run-github-launchd.sh +62 -0
  296. package/skill/run-github.sh +102 -0
  297. package/skill/run-instagram-daily.sh +149 -0
  298. package/skill/run-instagram-render.sh +875 -0
  299. package/skill/run-linkedin-launchd.sh +81 -0
  300. package/skill/run-linkedin-unipile.sh +130 -0
  301. package/skill/run-linkedin.sh +1593 -0
  302. package/skill/run-moltbook-launchd.sh +61 -0
  303. package/skill/run-moltbook.sh +38 -0
  304. package/skill/run-overlay-watch.sh +100 -0
  305. package/skill/run-reddit-search-launchd.sh +64 -0
  306. package/skill/run-reddit-search.sh +505 -0
  307. package/skill/run-reddit-threads-double.sh +32 -0
  308. package/skill/run-reddit-threads.sh +847 -0
  309. package/skill/run-scan-moltbook-replies.sh +57 -0
  310. package/skill/run-twitter-cycle-launchd.sh +63 -0
  311. package/skill/run-twitter-cycle-singleton.sh +62 -0
  312. package/skill/run-twitter-cycle.sh +2408 -0
  313. package/skill/run-twitter-threads.sh +592 -0
  314. package/skill/scan-instagram-replies.sh +61 -0
  315. package/skill/scan-twitter-followups.sh +57 -0
  316. package/skill/social-autoposter-update.sh +66 -0
  317. package/skill/stats-instagram.sh +72 -0
  318. package/skill/stats-linkedin.sh +271 -0
  319. package/skill/stats-moltbook.sh +4 -0
  320. package/skill/stats-reddit.sh +4 -0
  321. package/skill/stats-twitter.sh +4 -0
  322. package/skill/stats.sh +521 -0
  323. package/skill/strike-alert.sh +18 -0
  324. package/skill/styles.sh +87 -0
  325. package/skill/sweep-link-clicks.sh +40 -0
  326. package/skill/topics.sh +51 -0
@@ -0,0 +1,306 @@
1
+ // Telemetry for the .mcpb desktop client: install-lane heartbeat + Sentry error
2
+ // reporting. Both are best-effort and MUST never throw into the MCP server.
3
+ //
4
+ // Why this exists: the npx install lane registers a launchd heartbeat
5
+ // (com.m13v.social-autoposter-heartbeat) so installs show up in the
6
+ // install-lane digest. The .mcpb (Claude Desktop extension) had no equivalent,
7
+ // so .mcpb installs were invisible (and their errors uncollected). This module
8
+ // closes both gaps. Mirrors the Fazm app's Sentry posture (org `mediar-n5`).
9
+ import * as Sentry from "@sentry/node";
10
+ import path from "node:path";
11
+ import fs from "node:fs";
12
+ import { repoDir, runPython, setLineSink } from "./repo.js";
13
+ import { VERSION } from "./version.js";
14
+ // Sentry DSN is a client-side identifier (safe to embed, same posture as Fazm's
15
+ // hardcoded Swift DSN). Overridable via env for dev. Empty -> Sentry disabled.
16
+ const EMBEDDED_DSN = "https://4d44ac907262c6545cf8681703528d04@o4507617161314304.ingest.us.sentry.io/4511598804336640";
17
+ const SENTRY_DSN = process.env.S4L_SENTRY_DSN || process.env.SAPS_SENTRY_DSN || EMBEDDED_DSN;
18
+ let sentryReady = false;
19
+ export function initSentry() {
20
+ if (sentryReady || !SENTRY_DSN)
21
+ return;
22
+ try {
23
+ Sentry.init({
24
+ dsn: SENTRY_DSN,
25
+ release: `social-autoposter-mcp@${VERSION}`,
26
+ environment: (process.env.S4L_ENV ?? process.env.SAPS_ENV) === "development" || process.env.NODE_ENV === "development"
27
+ ? "development"
28
+ : "production",
29
+ // Errors only; no performance tracing (keeps the bundle's overhead minimal
30
+ // and avoids the OpenTelemetry --import requirement under ESM).
31
+ tracesSampleRate: 0,
32
+ sendDefaultPii: false,
33
+ });
34
+ sentryReady = true;
35
+ void tagInstall();
36
+ }
37
+ catch {
38
+ /* never let telemetry init break the server */
39
+ }
40
+ }
41
+ // Attach the stable install_id so Sentry events are attributable to an install
42
+ // (and cross-referenceable with the install-lane digest). Best-effort.
43
+ async function tagInstall() {
44
+ try {
45
+ const idScript = path.join(repoDir(), "scripts", "identity.py");
46
+ if (!fs.existsSync(idScript))
47
+ return;
48
+ const res = await runPython("scripts/identity.py", ["show"], { timeoutMs: 10_000 });
49
+ if (res.code !== 0)
50
+ return;
51
+ const id = JSON.parse(res.stdout || "{}");
52
+ if (id.install_id)
53
+ Sentry.setTag("install_id", String(id.install_id));
54
+ if (id.hostname)
55
+ Sentry.setTag("hostname", String(id.hostname));
56
+ }
57
+ catch {
58
+ /* best-effort */
59
+ }
60
+ }
61
+ export function captureError(err, tags) {
62
+ try {
63
+ if (sentryReady)
64
+ Sentry.captureException(err, tags ? { tags } : undefined);
65
+ }
66
+ catch {
67
+ /* swallow */
68
+ }
69
+ }
70
+ export async function flushSentry(ms = 2000) {
71
+ try {
72
+ if (sentryReady)
73
+ await Sentry.flush(ms);
74
+ }
75
+ catch {
76
+ /* swallow */
77
+ }
78
+ }
79
+ // Phone home so .mcpb installs show up in the install-lane digest, parity with
80
+ // the npx launchd heartbeat. Best-effort; never throws.
81
+ export async function sendHeartbeat(reason) {
82
+ try {
83
+ const idScript = path.join(repoDir(), "scripts", "identity.py");
84
+ if (!fs.existsSync(idScript))
85
+ return; // runtime not unpacked yet (pre-install)
86
+ const res = await runPython("scripts/identity.py", ["header"], { timeoutMs: 10_000 });
87
+ const header = (res.stdout || "").trim();
88
+ if (res.code !== 0 || !header)
89
+ return;
90
+ const base = (process.env.AUTOPOSTER_API_BASE || "https://s4l.ai").replace(/\/+$/, "");
91
+ // Attach a slim host-resource sample so a leaking box (the agent-mode
92
+ // session pile-up that can balloon RAM to tens of GB) is visible centrally
93
+ // without us SSHing in. Best-effort: any failure falls back to "{}" so the
94
+ // heartbeat itself never depends on the sampler succeeding.
95
+ const bodyObj = {};
96
+ try {
97
+ const mem = await runPython("scripts/memory_snapshot.py", ["--summary"], { timeoutMs: 12_000 });
98
+ const out = (mem.stdout || "").trim();
99
+ if (mem.code === 0 && out)
100
+ bodyObj.resource = JSON.parse(out);
101
+ }
102
+ catch {
103
+ /* omit resource */
104
+ }
105
+ // Also attach the S4L autopilot scheduled-task folder state so the server can
106
+ // tell, per install, whether the queue-worker tasks relocated to ~/.s4l-worker
107
+ // or are still mislocated (the menubar cwd-rewrite self-heal used to fire
108
+ // silently — no fleet-wide signal). Best-effort; independent of resource.
109
+ try {
110
+ const st = await runPython("scripts/scheduled_tasks_snapshot.py", ["--summary"], { timeoutMs: 10_000 });
111
+ const out = (st.stdout || "").trim();
112
+ if (st.code === 0 && out)
113
+ bodyObj.scheduled_tasks = JSON.parse(out);
114
+ }
115
+ catch {
116
+ /* omit scheduled_tasks */
117
+ }
118
+ const body = Object.keys(bodyObj).length ? JSON.stringify(bodyObj) : "{}";
119
+ const resp = await fetch(`${base}/api/v1/installations/heartbeat`, {
120
+ method: "POST",
121
+ headers: { "X-Installation": header, "content-type": "application/json" },
122
+ body,
123
+ signal: AbortSignal.timeout(15_000),
124
+ });
125
+ if (!resp.ok)
126
+ console.error(`[social-autoposter-mcp] heartbeat http ${resp.status}`);
127
+ }
128
+ catch (err) {
129
+ captureError(err, { component: "heartbeat", reason });
130
+ console.error("[social-autoposter-mcp] heartbeat failed:", err?.message || err);
131
+ }
132
+ }
133
+ // ---- Raw subprocess log streaming ------------------------------------------
134
+ // Tees the verbatim stdout/stderr of every pipeline subprocess (via the
135
+ // repo.ts run() boundary) to the s4l Cloud Run relay, which simply
136
+ // console.log()s each line so Cloud Run's runtime ships it to Cloud Logging.
137
+ // No database, no service-account key on the client — the relay is the only
138
+ // thing authenticated to GCP, and it authenticates implicitly via its Cloud
139
+ // Run runtime identity. Lines are buffered in memory and flushed in small
140
+ // batches under the same X-Installation identity the heartbeat uses.
141
+ //
142
+ // Best-effort: NEVER throws into the server, never blocks the child's I/O, and
143
+ // drops on overflow rather than growing unbounded. Disable with
144
+ // S4L_LOG_STREAM=0.
145
+ //
146
+ // IMPORTANT: logs go to the CLOUD RUN host (AUTOPOSTER_LOG_BASE, default
147
+ // app.s4l.ai), NOT the Vercel host (AUTOPOSTER_API_BASE / s4l.ai) the heartbeat
148
+ // and onboarding-events use. Cloud Run's native stdout -> Cloud Logging path is
149
+ // the whole point of this lane.
150
+ const LOG_STREAM_ENABLED = (process.env.S4L_LOG_STREAM ?? process.env.SAPS_LOG_STREAM) !== "0";
151
+ const LOG_MAX_LINE_LEN = 8192; // mirror the relay cap
152
+ const LOG_MAX_BUFFER = 1000; // drop oldest beyond this (overflow protection)
153
+ const LOG_FLUSH_BATCH = 100; // flush eagerly once we have this many lines
154
+ const LOG_MAX_PER_POST = 200; // relay accepts 1-200 per request
155
+ const LOG_FLUSH_MS = 3000; // otherwise flush on this cadence
156
+ // Drop genuinely useless high-volume lines before they ever buffer, so a chatty
157
+ // run doesn't crowd out the signal (and to keep Cloud Logging volume sane).
158
+ // Empty/whitespace-only lines plus an env-extensible regex of obvious dump
159
+ // signatures. Deliberately conservative: real pipeline output is the value, so
160
+ // we only filter clear noise. Extend via S4L_LOG_NOISE_RE (a JS regex source).
161
+ let logNoiseRe = null;
162
+ try {
163
+ const extra = (process.env.S4L_LOG_NOISE_RE || process.env.SAPS_LOG_NOISE_RE || "").trim();
164
+ const sources = [
165
+ extra,
166
+ ].filter(Boolean);
167
+ logNoiseRe = sources.length ? new RegExp(sources.join("|")) : null;
168
+ }
169
+ catch {
170
+ logNoiseRe = null;
171
+ }
172
+ // The X-Installation header (identity.py `header` output) is a single long base64
173
+ // blob printed on stdout. Every heartbeat + every log flush shells identity.py to
174
+ // mint it, and that stdout was being tee'd straight back into the log stream, which
175
+ // re-triggered a flush — a self-referential loop that flooded Cloud Logging with
176
+ // ~21k identical base64 lines/hour and buried real pipeline output. Karol's box was
177
+ // impossible to read through it. Drop any line that is nothing but a long run of
178
+ // base64 chars (no spaces): real pipeline output is never shaped like this, so the
179
+ // filter is safe. Kept separate from logNoiseRe so an env override can't disable it.
180
+ const BASE64_BLOB_RE = /^[A-Za-z0-9+/=_-]{120,}$/;
181
+ function isNoise(line) {
182
+ if (!line || !line.trim())
183
+ return true; // blank / whitespace-only
184
+ if (BASE64_BLOB_RE.test(line.trim()))
185
+ return true; // X-Installation header echo
186
+ if (logNoiseRe && logNoiseRe.test(line))
187
+ return true;
188
+ return false;
189
+ }
190
+ const logBuffer = [];
191
+ let logDropped = 0; // count of lines dropped on overflow (surfaced periodically)
192
+ let logFlushing = false;
193
+ let logTimer;
194
+ let cachedInstallHeader = null;
195
+ let logStreamingStarted = false;
196
+ async function installHeader() {
197
+ if (cachedInstallHeader)
198
+ return cachedInstallHeader;
199
+ try {
200
+ const idScript = path.join(repoDir(), "scripts", "identity.py");
201
+ if (!fs.existsSync(idScript))
202
+ return null;
203
+ const res = await runPython("scripts/identity.py", ["header"], { timeoutMs: 10_000 });
204
+ const header = (res.stdout || "").trim();
205
+ if (res.code === 0 && header) {
206
+ cachedInstallHeader = header;
207
+ return header;
208
+ }
209
+ }
210
+ catch {
211
+ /* best-effort */
212
+ }
213
+ return null;
214
+ }
215
+ // Buffer one raw line. Called from the repo.ts line sink, so it must be cheap
216
+ // and total non-throwing.
217
+ export function logLine(stream, line, context) {
218
+ if (!LOG_STREAM_ENABLED)
219
+ return;
220
+ try {
221
+ if (isNoise(line))
222
+ return;
223
+ logBuffer.push({
224
+ ts: new Date().toISOString(),
225
+ stream,
226
+ line: line.length > LOG_MAX_LINE_LEN ? line.slice(0, LOG_MAX_LINE_LEN) : line,
227
+ context: context || "",
228
+ });
229
+ if (logBuffer.length > LOG_MAX_BUFFER) {
230
+ // Drop oldest to bound memory; the newest lines are the most useful.
231
+ logDropped += logBuffer.length - LOG_MAX_BUFFER;
232
+ logBuffer.splice(0, logBuffer.length - LOG_MAX_BUFFER);
233
+ }
234
+ if (logBuffer.length >= LOG_FLUSH_BATCH)
235
+ void flushLogs();
236
+ }
237
+ catch {
238
+ /* never throw into the run() boundary */
239
+ }
240
+ }
241
+ export async function flushLogs() {
242
+ if (!LOG_STREAM_ENABLED)
243
+ return;
244
+ if (logFlushing || logBuffer.length === 0)
245
+ return;
246
+ logFlushing = true;
247
+ try {
248
+ const header = await installHeader();
249
+ if (!header)
250
+ return; // runtime not unpacked yet; keep buffering
251
+ // Cloud Run relay host (NOT the Vercel API host). app.s4l.ai serves
252
+ // bin/server.js, whose POST /api/v1/installations/logs console.log()s each
253
+ // line into Cloud Logging.
254
+ const base = (process.env.AUTOPOSTER_LOG_BASE || "https://app.s4l.ai").replace(/\/+$/, "");
255
+ // Drain in <=200-line POSTs until the buffer empties (or a POST fails).
256
+ while (logBuffer.length > 0) {
257
+ const batch = logBuffer.splice(0, LOG_MAX_PER_POST);
258
+ const lines = batch.map((b) => ({
259
+ ts: b.ts,
260
+ stream: b.stream,
261
+ line: b.line,
262
+ context: b.context || undefined,
263
+ }));
264
+ try {
265
+ const resp = await fetch(`${base}/api/v1/installations/logs`, {
266
+ method: "POST",
267
+ headers: { "X-Installation": header, "content-type": "application/json" },
268
+ body: JSON.stringify({ lines }),
269
+ signal: AbortSignal.timeout(15_000),
270
+ });
271
+ if (!resp.ok) {
272
+ // Drop this batch (don't re-buffer): a persistent 4xx/5xx would grow
273
+ // the buffer unbounded. The raw stream is best-effort.
274
+ console.error(`[social-autoposter-mcp] log flush http ${resp.status}`);
275
+ break;
276
+ }
277
+ }
278
+ catch (err) {
279
+ // Network blip: drop this batch, stop draining, try again next tick.
280
+ console.error("[social-autoposter-mcp] log flush failed:", err?.message || err);
281
+ break;
282
+ }
283
+ }
284
+ if (logDropped > 0) {
285
+ console.error(`[social-autoposter-mcp] log stream dropped ${logDropped} line(s) on overflow`);
286
+ logDropped = 0;
287
+ }
288
+ }
289
+ finally {
290
+ logFlushing = false;
291
+ }
292
+ }
293
+ // Register the repo.ts line sink and start the periodic flush. Idempotent.
294
+ export function startLogStreaming() {
295
+ if (!LOG_STREAM_ENABLED || logStreamingStarted)
296
+ return;
297
+ logStreamingStarted = true;
298
+ try {
299
+ setLineSink((line, stream, context) => logLine(stream, line, context));
300
+ logTimer = setInterval(() => void flushLogs(), LOG_FLUSH_MS);
301
+ logTimer.unref();
302
+ }
303
+ catch (err) {
304
+ console.error("[social-autoposter-mcp] log streaming start failed:", err?.message || err);
305
+ }
306
+ }
@@ -0,0 +1,138 @@
1
+ // Twitter/X session bootstrap for the setup flow.
2
+ //
3
+ // Thin wrapper over scripts/setup_twitter_auth.py, which owns the real work
4
+ // (ensure the managed Chrome on CDP 9555, validate the x.com session, and, if
5
+ // logged out, import x.com/twitter.com cookies from the user's everyday
6
+ // browser via ai_browser_profile.cookies). We only shell out and parse JSON.
7
+ //
8
+ // Why a separate Python helper instead of doing CDP here: the validation +
9
+ // cookie-import primitives already exist and are battle-tested in the repo
10
+ // (restore_twitter_session.py for CDP login-check, ai_browser_profile.cookies
11
+ // for Keychain-decrypt + CDP inject). Reusing them keeps this MCP a thin client.
12
+ import { runPython } from "./repo.js";
13
+ import { captureError } from "./telemetry.js";
14
+ function parse(stdout, stderr, code) {
15
+ try {
16
+ return JSON.parse(stdout.trim().split("\n").slice(-50).join("\n"));
17
+ }
18
+ catch {
19
+ return {
20
+ ok: false,
21
+ connected: false,
22
+ state: "error",
23
+ error: `setup_twitter_auth.py produced no parseable JSON (exit ${code}).\n` +
24
+ (stderr || stdout).split("\n").slice(-8).join("\n"),
25
+ };
26
+ }
27
+ }
28
+ // Probe-only: is the managed X session valid right now? Does NOT launch Chrome.
29
+ export async function xStatus() {
30
+ const res = await runPython("scripts/setup_twitter_auth.py", ["status"], {
31
+ timeoutMs: 90_000,
32
+ });
33
+ return parse(res.stdout, res.stderr, res.code);
34
+ }
35
+ // Ensure the browser is up, validate, and import cookies from the user's
36
+ // everyday browser if needed. `source` optional (e.g. "arc:Default"); default
37
+ // auto-detects chrome/arc/brave/edge.
38
+ export async function xConnect(source, manualLogin) {
39
+ const args = ["connect"];
40
+ if (source)
41
+ args.push("--source", source);
42
+ // Only pop a Chrome login window when the user explicitly asked to sign in by
43
+ // hand. Without this, auto-import failures (no X session in the browser, etc.)
44
+ // return needs_login WITHOUT shoving an unexpected browser window in front of
45
+ // the user; the login window still opens on its own if they DENIED keychain.
46
+ if (manualLogin)
47
+ args.push("--manual-login");
48
+ const res = await runPython("scripts/setup_twitter_auth.py", args, {
49
+ // import opens a real Chrome and may pop a macOS Keychain auth dialog the
50
+ // user has to find + click ("Always Allow"). Keep this above the Python
51
+ // cookie-copy timeout (S4L_COOKIE_COPY_TIMEOUT, default 600s) so the
52
+ // wrapper never kills the dialog before the human can.
53
+ timeoutMs: 660_000,
54
+ });
55
+ return parse(res.stdout, res.stderr, res.code);
56
+ }
57
+ export async function xScanProfile(opts) {
58
+ const args = [];
59
+ if (opts?.handle)
60
+ args.push("--handle", opts.handle);
61
+ args.push("--posts", String(opts?.posts ?? 20));
62
+ args.push("--comments", String(opts?.comments ?? 50));
63
+ // The scan scrolls two timelines; give it room but keep it bounded.
64
+ const res = await runPython("scripts/scan_x_profile.py", args, { timeoutMs: 180_000 });
65
+ try {
66
+ return JSON.parse(res.stdout.trim().split("\n").slice(-1).join("\n"));
67
+ }
68
+ catch (e) {
69
+ // The X profile scan feeds handle detection + grounding for the draft lane; a
70
+ // silent no-JSON failure here means we scrape the wrong handle (or none) and
71
+ // never know. Surface it so we can see fleet-wide how often the scan breaks.
72
+ captureError(e, {
73
+ component: "twitter_auth",
74
+ phase: "scan_x_profile",
75
+ exit: String(res.code),
76
+ });
77
+ return {
78
+ ok: false,
79
+ state: "error",
80
+ error: `scan_x_profile.py produced no parseable JSON (exit ${res.code}).\n` +
81
+ (res.stderr || res.stdout).split("\n").slice(-8).join("\n"),
82
+ };
83
+ }
84
+ }
85
+ // List browsers/profiles to import from. Read-only: NEVER reads the keychain or
86
+ // decrypts a cookie, so it shows no macOS Safe Storage prompt. Used to populate
87
+ // the panel's "import from" dropdown and to flag which profile has a live session.
88
+ export async function xDetectSources() {
89
+ const res = await runPython("scripts/setup_twitter_auth.py", ["detect-sources"], {
90
+ timeoutMs: 30_000,
91
+ });
92
+ try {
93
+ return JSON.parse(res.stdout.trim().split("\n").slice(-200).join("\n"));
94
+ }
95
+ catch (e) {
96
+ // detect-sources populates the panel's "import from" dropdown; a no-JSON failure
97
+ // leaves the user unable to connect X during setup with no server-side trace.
98
+ captureError(e, {
99
+ component: "twitter_auth",
100
+ phase: "detect_sources",
101
+ exit: String(res.code),
102
+ });
103
+ return {
104
+ ok: false,
105
+ sources: [],
106
+ error: `detect-sources produced no parseable JSON (exit ${res.code}).\n` +
107
+ (res.stderr || res.stdout).split("\n").slice(-8).join("\n"),
108
+ };
109
+ }
110
+ }
111
+ // One-line human summary for tool output.
112
+ export function summarizeXAuth(r) {
113
+ switch (r.state) {
114
+ case "connected":
115
+ return "X is connected (the autoposter browser has a valid x.com session).";
116
+ case "connected_idle":
117
+ return ("X is connected (your session is saved). The autoposter's browser isn't " +
118
+ "running this moment; the next cycle restores it from the local mirror " +
119
+ "automatically — no action needed.");
120
+ case "imported":
121
+ return `X connected — imported your session from ${r.source ?? "your browser"}.`;
122
+ case "logged_out":
123
+ return "X is not connected: the autoposter browser has no valid x.com session yet.";
124
+ case "browser_not_running":
125
+ return "The autoposter's X browser isn't running yet.";
126
+ case "needs_login":
127
+ // Prefer the helper's note: it says whether the login window actually
128
+ // came to the front and carries the full manual-login instructions.
129
+ return (r.note ??
130
+ "Couldn't import a valid X session automatically. A Chrome window is open at " +
131
+ "the X login page — sign in there yourself (username, password, 2FA), then " +
132
+ "run connect_x again to confirm.");
133
+ case "browser_launch_failed":
134
+ return r.error ?? "Could not start the autoposter browser.";
135
+ default:
136
+ return r.error ?? `X auth state: ${r.state}`;
137
+ }
138
+ }