@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,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
|
+
}
|