@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,518 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const { execSync, spawnSync } = require('child_process');
6
+ const platform = require('../platform');
7
+
8
+ // Unit filter for discovery and listing. Historically everything was
9
+ // `com.m13v.social-*`. A few SEO jobs got provisioned as `com.m13v.seo-*`
10
+ // (weekly roundup, standalone SEO daily report), so accept either prefix.
11
+ // Other m13v jobs (fazm-*, gmail-*, etc.) still get excluded.
12
+ const UNIT_PREFIXES = ['com.m13v.social-', 'com.m13v.seo-'];
13
+ const UNIT_PREFIX = UNIT_PREFIXES[0]; // kept for renderPlist/install callers
14
+ const UNIT_SUFFIX = '.plist';
15
+
16
+ function hasUnitPrefix(label) {
17
+ return UNIT_PREFIXES.some(p => label.startsWith(p));
18
+ }
19
+
20
+ function fileHasUnitPrefix(filename) {
21
+ return UNIT_PREFIXES.some(p => filename.startsWith(p)) && filename.endsWith(UNIT_SUFFIX);
22
+ }
23
+
24
+ function renderPlist(job, env) {
25
+ return `<?xml version="1.0" encoding="UTF-8"?>
26
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
27
+ <plist version="1.0">
28
+ <dict>
29
+ \t<key>Label</key>
30
+ \t<string>${job.label}</string>
31
+ \t<key>ProgramArguments</key>
32
+ \t<array>
33
+ \t\t<string>/bin/bash</string>
34
+ \t\t<string>${job.script}</string>
35
+ \t</array>
36
+ \t<key>StartInterval</key>
37
+ \t<integer>${job.interval}</integer>
38
+ \t<key>StandardOutPath</key>
39
+ \t<string>${job.stdoutLog}</string>
40
+ \t<key>StandardErrorPath</key>
41
+ \t<string>${job.stderrLog}</string>
42
+ \t<key>EnvironmentVariables</key>
43
+ \t<dict>
44
+ \t\t<key>PATH</key>
45
+ \t\t<string>${env.path}</string>
46
+ \t\t<key>HOME</key>
47
+ \t\t<string>${env.home}</string>
48
+ \t</dict>
49
+ \t<key>RunAtLoad</key>
50
+ \t<${job.runAtLoad}/>
51
+ </dict>
52
+ </plist>
53
+ `;
54
+ }
55
+
56
+ function generate({ jobs, outDir, env }) {
57
+ fs.mkdirSync(outDir, { recursive: true });
58
+ const written = [];
59
+ for (const job of jobs) {
60
+ const xml = renderPlist(job, env);
61
+ const target = path.join(outDir, job.file);
62
+ fs.writeFileSync(target, xml);
63
+ written.push(target);
64
+ }
65
+ return written;
66
+ }
67
+
68
+ function defaultEnv({ home, nodeBin }) {
69
+ return {
70
+ home,
71
+ path: platform.launchdPath(nodeBin),
72
+ };
73
+ }
74
+
75
+ // ─────────────────────────── Control plane ───────────────────────────
76
+
77
+ function list() {
78
+ const loadedLabels = new Set();
79
+ const pidByLabel = new Map();
80
+ try {
81
+ const out = execSync('launchctl list', { stdio: 'pipe', maxBuffer: 8 * 1024 * 1024 }).toString();
82
+ for (const line of out.split('\n').slice(1)) {
83
+ const parts = line.split('\t');
84
+ if (parts.length < 3) continue;
85
+ const label = parts[2];
86
+ if (!hasUnitPrefix(label)) continue;
87
+ loadedLabels.add(label);
88
+ const pid = parseInt(parts[0], 10);
89
+ if (!isNaN(pid)) pidByLabel.set(label, pid);
90
+ }
91
+ } catch {}
92
+ return { loadedLabels, pidByLabel };
93
+ }
94
+
95
+ function isLoaded(label) {
96
+ try {
97
+ execSync(`launchctl list ${label}`, { stdio: 'pipe' });
98
+ return true;
99
+ } catch { return false; }
100
+ }
101
+
102
+ function pidFor(label) {
103
+ try {
104
+ const out = execSync(`launchctl list ${label}`, { stdio: 'pipe' }).toString();
105
+ const m = out.match(/"PID"\s*=\s*(\d+);/);
106
+ return m ? parseInt(m[1], 10) : null;
107
+ } catch { return null; }
108
+ }
109
+
110
+ // launchctl load/unload exit 0 even on failure (e.g. "Unload failed: 5:
111
+ // Input/output error" when already unloaded). Use spawnSync so we capture
112
+ // stderr and detect silent-failure cases.
113
+ function load(unitPath) {
114
+ const r = spawnSync('launchctl', ['load', unitPath], { encoding: 'utf8' });
115
+ const stderr = (r.stderr || '').trim();
116
+ return { ok: r.status === 0 && !/failed/i.test(stderr), stderr, status: r.status };
117
+ }
118
+
119
+ function unload(_label, unitPath) {
120
+ const r = spawnSync('launchctl', ['unload', unitPath], { encoding: 'utf8' });
121
+ const stderr = (r.stderr || '').trim();
122
+ return { ok: r.status === 0 && !/failed/i.test(stderr), stderr, status: r.status };
123
+ }
124
+
125
+ function kickstart(label) {
126
+ const target = `gui/${process.getuid()}/${label}`;
127
+ const r = spawnSync('launchctl', ['kickstart', '-p', target], { encoding: 'utf8' });
128
+ const pid = parseInt((r.stdout || '').trim(), 10);
129
+ return {
130
+ ok: r.status === 0,
131
+ stderr: (r.stderr || r.stdout || '').trim(),
132
+ pid: isNaN(pid) ? null : pid,
133
+ };
134
+ }
135
+
136
+ function killJob(label) {
137
+ const target = `gui/${process.getuid()}/${label}`;
138
+ const r = spawnSync('launchctl', ['kill', 'SIGKILL', target], { encoding: 'utf8' });
139
+ return { ok: r.status === 0, stderr: (r.stderr || '').trim() };
140
+ }
141
+
142
+ // Install a unit file into the user's agents dir (via symlink, matching the
143
+ // existing setup flow). Creates the dir if missing.
144
+ function install(unitSrc, agentsDir) {
145
+ fs.mkdirSync(agentsDir, { recursive: true });
146
+ const linkPath = path.join(agentsDir, path.basename(unitSrc));
147
+ if (!fs.existsSync(linkPath)) {
148
+ try { fs.symlinkSync(unitSrc, linkPath); } catch { return null; }
149
+ }
150
+ return linkPath;
151
+ }
152
+
153
+ function unitFileName(jobFile) {
154
+ // jobFile is e.g. "com.m13v.social-stats.plist"; launchd needs the plist path.
155
+ return jobFile;
156
+ }
157
+
158
+ // Discover every social-autoposter job from plist files in either the repo's
159
+ // launchd/ dir or the user's LaunchAgents dir. Returns [{label, unitFile, scriptPath}].
160
+ function discoverJobs({ repoUnitDir, agentsDir }) {
161
+ const byLabel = new Map();
162
+ const scan = (dir) => {
163
+ try {
164
+ const files = fs.readdirSync(dir).filter(fileHasUnitPrefix);
165
+ for (const f of files) {
166
+ try {
167
+ const body = fs.readFileSync(path.join(dir, f), 'utf8');
168
+ const { label, scriptPath } = parseUnit(body);
169
+ if (!label) continue;
170
+ if (!byLabel.has(label)) {
171
+ byLabel.set(label, { label, unitFile: f, scriptPath });
172
+ }
173
+ } catch {}
174
+ }
175
+ } catch {}
176
+ };
177
+ scan(repoUnitDir);
178
+ scan(agentsDir);
179
+ return [...byLabel.values()];
180
+ }
181
+
182
+ function parseUnit(xml) {
183
+ const labelM = xml.match(/<key>Label<\/key>\s*<string>([^<]+)<\/string>/);
184
+ const label = labelM ? labelM[1] : null;
185
+ let scriptPath = null;
186
+ const argsM = xml.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
187
+ if (argsM) {
188
+ const strings = [...argsM[1].matchAll(/<string>([^<]+)<\/string>/g)].map(m => m[1]);
189
+ scriptPath = strings.find(s => /\.(sh|py|js)$/.test(s)) || null;
190
+ }
191
+ if (!scriptPath) {
192
+ const progM = xml.match(/<key>Program<\/key>\s*<string>([^<]+)<\/string>/);
193
+ if (progM) scriptPath = progM[1];
194
+ }
195
+ return { label, scriptPath };
196
+ }
197
+
198
+ // Returns interval in seconds, or null if calendar-based / unsettable.
199
+ function scheduleFromUnit(xml) {
200
+ try {
201
+ const si = xml.match(/<key>StartInterval<\/key>\s*<integer>(\d+)<\/integer>/);
202
+ if (si) return { intervalSecs: parseInt(si[1], 10), kind: 'simple' };
203
+ let entries = null;
204
+ const arrM = xml.match(/<key>StartCalendarInterval<\/key>\s*<array>([\s\S]*?)<\/array>/);
205
+ if (arrM) {
206
+ entries = [...arrM[1].matchAll(/<dict>([\s\S]*?)<\/dict>/g)].map(m => m[1]);
207
+ } else {
208
+ const dictM = xml.match(/<key>StartCalendarInterval<\/key>\s*<dict>([\s\S]*?)<\/dict>/);
209
+ if (dictM) entries = [dictM[1]];
210
+ }
211
+ if (!entries || !entries.length) return { intervalSecs: null, kind: null };
212
+ const dayMins = [];
213
+ const weeklyMins = [];
214
+ let minuteOnlyCount = 0;
215
+ for (const body of entries) {
216
+ const h = body.match(/<key>Hour<\/key>\s*<integer>(\d+)<\/integer>/);
217
+ const m = body.match(/<key>Minute<\/key>\s*<integer>(\d+)<\/integer>/);
218
+ const w = body.match(/<key>Weekday<\/key>\s*<integer>(\d+)<\/integer>/);
219
+ if (h && w) {
220
+ const wd = parseInt(w[1], 10) % 7;
221
+ weeklyMins.push(wd * 1440 + parseInt(h[1], 10) * 60 + (m ? parseInt(m[1], 10) : 0));
222
+ } else if (h) {
223
+ dayMins.push(parseInt(h[1], 10) * 60 + (m ? parseInt(m[1], 10) : 0));
224
+ } else if (m) {
225
+ minuteOnlyCount++;
226
+ }
227
+ }
228
+ if (weeklyMins.length === 1) return { intervalSecs: 604800, kind: 'calendar' };
229
+ if (weeklyMins.length > 1) {
230
+ weeklyMins.sort((a, b) => a - b);
231
+ let minGap = Infinity;
232
+ for (let i = 1; i < weeklyMins.length; i++) {
233
+ minGap = Math.min(minGap, weeklyMins[i] - weeklyMins[i - 1]);
234
+ }
235
+ minGap = Math.min(minGap, 10080 - weeklyMins[weeklyMins.length - 1] + weeklyMins[0]);
236
+ return { intervalSecs: minGap * 60, kind: 'calendar' };
237
+ }
238
+ if (minuteOnlyCount > 0 && dayMins.length === 0) {
239
+ return { intervalSecs: Math.round(3600 / minuteOnlyCount), kind: 'calendar' };
240
+ }
241
+ if (dayMins.length === 1) return { intervalSecs: 86400, kind: 'calendar' };
242
+ if (dayMins.length > 1) {
243
+ // Use the AVERAGE gap (86400/N) rather than the minimum gap. The min-gap
244
+ // representation lies for unevenly-spaced calendar schedules: e.g. fires
245
+ // at 8/12/17 have gaps of 4h/5h/15h, and labeling that "every 4h" implies
246
+ // 6 fires/day when there are actually 3. 86400/N round-trips cleanly to
247
+ // INTERVALS labels like "3 times a day".
248
+ return { intervalSecs: Math.round(86400 / dayMins.length), kind: 'calendar' };
249
+ }
250
+ return { intervalSecs: null, kind: null };
251
+ } catch { return { intervalSecs: null, kind: null }; }
252
+ }
253
+
254
+ // Returns a Date for the next scheduled fire in the host's local timezone, or
255
+ // null if the unit has no settable schedule (e.g. KeepAlive-only, or a calendar
256
+ // entry we can't reduce to a concrete next-fire). Handles:
257
+ // - StartInterval: now + intervalSecs
258
+ // - StartCalendarInterval with a single Hour/Minute dict: next occurrence
259
+ // today at HH:MM (or tomorrow if HH:MM has already passed)
260
+ // - StartCalendarInterval as an array of Hour/Minute dicts: earliest upcoming
261
+ // entry across today/tomorrow
262
+ function nextRunFromUnit(xml) {
263
+ try {
264
+ const si = xml.match(/<key>StartInterval<\/key>\s*<integer>(\d+)<\/integer>/);
265
+ if (si) {
266
+ const secs = parseInt(si[1], 10);
267
+ if (!secs) return null;
268
+ return new Date(Date.now() + secs * 1000);
269
+ }
270
+ let entries = null;
271
+ const arrM = xml.match(/<key>StartCalendarInterval<\/key>\s*<array>([\s\S]*?)<\/array>/);
272
+ if (arrM) {
273
+ entries = [...arrM[1].matchAll(/<dict>([\s\S]*?)<\/dict>/g)].map(m => m[1]);
274
+ } else {
275
+ const dictM = xml.match(/<key>StartCalendarInterval<\/key>\s*<dict>([\s\S]*?)<\/dict>/);
276
+ if (dictM) entries = [dictM[1]];
277
+ }
278
+ if (!entries || !entries.length) return null;
279
+ const hhmms = [];
280
+ for (const body of entries) {
281
+ const h = body.match(/<key>Hour<\/key>\s*<integer>(\d+)<\/integer>/);
282
+ const m = body.match(/<key>Minute<\/key>\s*<integer>(\d+)<\/integer>/);
283
+ const w = body.match(/<key>Weekday<\/key>\s*<integer>(\d+)<\/integer>/);
284
+ if (h) hhmms.push({
285
+ hour: parseInt(h[1], 10),
286
+ minute: m ? parseInt(m[1], 10) : 0,
287
+ weekday: w ? (parseInt(w[1], 10) % 7) : null,
288
+ });
289
+ }
290
+ if (!hhmms.length) return null;
291
+ const now = new Date();
292
+ let best = null;
293
+ for (const { hour, minute, weekday } of hhmms) {
294
+ const cand = new Date(now);
295
+ cand.setHours(hour, minute, 0, 0);
296
+ if (weekday != null) {
297
+ let dayDelta = (weekday - cand.getDay() + 7) % 7;
298
+ if (dayDelta === 0 && cand <= now) dayDelta = 7;
299
+ cand.setDate(cand.getDate() + dayDelta);
300
+ } else if (cand <= now) {
301
+ cand.setDate(cand.getDate() + 1);
302
+ }
303
+ if (!best || cand < best) best = cand;
304
+ }
305
+ return best;
306
+ } catch { return null; }
307
+ }
308
+
309
+ // Set the schedule on a plist to a fixed StartInterval. Handles both forms:
310
+ // - StartInterval already present -> in-place edit the integer
311
+ // - StartCalendarInterval (array or single dict) -> replace the whole block
312
+ // with StartInterval. This is what makes the dashboard dropdown the true
313
+ // single source of truth across every Post Comments / Engage / etc. row,
314
+ // since most plists are calendar-form and used to silently fail to update.
315
+ // Returns true on success.
316
+ function updateInterval(unitPath, seconds) {
317
+ const xml = fs.readFileSync(unitPath, 'utf8');
318
+ const replacement = `<key>StartInterval</key>\n\t<integer>${seconds}</integer>`;
319
+ if (/<key>StartInterval<\/key>/.test(xml)) {
320
+ const next = xml.replace(
321
+ /(<key>StartInterval<\/key>\s*<integer>)\d+(<\/integer>)/,
322
+ `$1${seconds}$2`
323
+ );
324
+ fs.writeFileSync(unitPath, next);
325
+ return true;
326
+ }
327
+ const arrM = /<key>StartCalendarInterval<\/key>\s*<array>[\s\S]*?<\/array>/;
328
+ if (arrM.test(xml)) {
329
+ fs.writeFileSync(unitPath, xml.replace(arrM, replacement));
330
+ return true;
331
+ }
332
+ const dictM = /<key>StartCalendarInterval<\/key>\s*<dict>[\s\S]*?<\/dict>/;
333
+ if (dictM.test(xml)) {
334
+ fs.writeFileSync(unitPath, xml.replace(dictM, replacement));
335
+ return true;
336
+ }
337
+ return false;
338
+ }
339
+
340
+ // Read the "start time" from a plist: for a single-dict calendar schedule,
341
+ // the Hour/Minute; for an array-form calendar schedule, the earliest entry.
342
+ // Returns null for interval-only jobs (no wall-clock anchor).
343
+ function startTimeFromUnit(xml) {
344
+ try {
345
+ const arrM = xml.match(/<key>StartCalendarInterval<\/key>\s*<array>([\s\S]*?)<\/array>/);
346
+ if (arrM) {
347
+ // Return the first entry in document order, which is the user-chosen
348
+ // anchor when this plist was written by updateStartTime. For legacy
349
+ // hand-written arrays (typically sorted ascending) this also happens to
350
+ // be the earliest fire of the day.
351
+ const first = arrM[1].match(/<dict>([\s\S]*?)<\/dict>/);
352
+ if (!first) return null;
353
+ const fh = first[1].match(/<key>Hour<\/key>\s*<integer>(\d+)<\/integer>/);
354
+ const fm = first[1].match(/<key>Minute<\/key>\s*<integer>(\d+)<\/integer>/);
355
+ if (!fh && !fm) return null;
356
+ return {
357
+ hour: fh ? parseInt(fh[1], 10) : 0,
358
+ minute: fm ? parseInt(fm[1], 10) : 0,
359
+ };
360
+ }
361
+ const dictM = xml.match(/<key>StartCalendarInterval<\/key>\s*<dict>([\s\S]*?)<\/dict>/);
362
+ if (!dictM) return null;
363
+ const body = dictM[1];
364
+ const h = body.match(/<key>Hour<\/key>\s*<integer>(\d+)<\/integer>/);
365
+ const m = body.match(/<key>Minute<\/key>\s*<integer>(\d+)<\/integer>/);
366
+ if (!h && !m) return null;
367
+ return {
368
+ hour: h ? parseInt(h[1], 10) : 0,
369
+ minute: m ? parseInt(m[1], 10) : 0,
370
+ };
371
+ } catch { return null; }
372
+ }
373
+
374
+ // Count the fires-per-day implied by a plist's schedule, and the cadence
375
+ // (minutes between fires). Used to decide whether a user-supplied start time
376
+ // produces a single-fire or a multi-fire calendar array.
377
+ // Returns { count, cadenceMin }. count=1 means a single daily/weekly fire;
378
+ // count>1 means a multi-fire array.
379
+ function cadenceFromUnit(xml) {
380
+ const arrM = xml.match(/<key>StartCalendarInterval<\/key>\s*<array>([\s\S]*?)<\/array>/);
381
+ if (arrM) {
382
+ const entries = [...arrM[1].matchAll(/<dict>/g)];
383
+ const count = Math.max(1, entries.length);
384
+ return { count, cadenceMin: count > 1 ? Math.round(1440 / count) : null };
385
+ }
386
+ const dictM = xml.match(/<key>StartCalendarInterval<\/key>\s*<dict>/);
387
+ if (dictM) return { count: 1, cadenceMin: null };
388
+ const siM = xml.match(/<key>StartInterval<\/key>\s*<integer>(\d+)<\/integer>/);
389
+ if (siM) {
390
+ const secs = parseInt(siM[1], 10);
391
+ if (secs <= 0) return { count: 1, cadenceMin: null };
392
+ if (secs >= 86400) return { count: 1, cadenceMin: null };
393
+ return {
394
+ count: Math.max(1, Math.floor(86400 / secs)),
395
+ cadenceMin: Math.max(1, Math.round(secs / 60)),
396
+ };
397
+ }
398
+ return { count: 1, cadenceMin: null };
399
+ }
400
+
401
+ // Shift a plist's start time to {hour, minute}, preserving cadence and count.
402
+ // - Single-dict calendar (incl. Weekday-qualified weekly jobs): surgical
403
+ // H/M rewrite so Weekday (and any other keys) are preserved.
404
+ // - Array-form calendar or StartInterval sub-daily: replace the schedule
405
+ // with an evenly-spaced array of the same count, starting at HH:MM.
406
+ // - StartInterval >= 1 day or no schedule: emit a single daily dict.
407
+ // Returns { ok, kind, count } on success, { ok: false, reason } otherwise.
408
+ // Caller is responsible for reloading the job so launchd picks up the change.
409
+ function updateStartTime(unitPath, hour, minute) {
410
+ const xml = fs.readFileSync(unitPath, 'utf8');
411
+ const h = Math.max(0, Math.min(23, parseInt(hour, 10)));
412
+ const m = Math.max(0, Math.min(59, parseInt(minute, 10)));
413
+ if (Number.isNaN(h) || Number.isNaN(m)) return { ok: false, reason: 'invalid time' };
414
+
415
+ // Case 1: single-dict calendar — edit Hour/Minute in place so extra keys
416
+ // (Weekday for weekly jobs, etc.) survive untouched.
417
+ const hasArray = /<key>StartCalendarInterval<\/key>\s*<array>/.test(xml);
418
+ if (!hasArray) {
419
+ const singleM = xml.match(/(<key>StartCalendarInterval<\/key>[ \t\r\n]*<dict>)([\s\S]*?)(<\/dict>)/);
420
+ if (singleM) {
421
+ let body = singleM[2];
422
+ const hadHour = /<key>Hour<\/key>/.test(body);
423
+ const hadMin = /<key>Minute<\/key>/.test(body);
424
+ if (hadHour) {
425
+ body = body.replace(/(<key>Hour<\/key>[ \t\r\n]*<integer>)\d+(<\/integer>)/, `$1${h}$2`);
426
+ }
427
+ if (hadMin) {
428
+ body = body.replace(/(<key>Minute<\/key>[ \t\r\n]*<integer>)\d+(<\/integer>)/, `$1${m}$2`);
429
+ }
430
+ if (!hadHour || !hadMin) {
431
+ // Rare: single-dict schedule with only Weekday. Append missing keys.
432
+ const indent = detectIndent(xml);
433
+ const inner = indent + indent;
434
+ const trimmed = body.replace(/\s+$/, '');
435
+ const addHour = hadHour ? '' : `\n${inner}<key>Hour</key>\n${inner}<integer>${h}</integer>`;
436
+ const addMin = hadMin ? '' : `\n${inner}<key>Minute</key>\n${inner}<integer>${m}</integer>`;
437
+ body = trimmed + addHour + addMin + `\n${indent}`;
438
+ }
439
+ const next = xml.replace(singleM[0], singleM[1] + body + singleM[3]);
440
+ fs.writeFileSync(unitPath, next);
441
+ return { ok: true, kind: 'single', count: 1 };
442
+ }
443
+ }
444
+
445
+ // Case 2: array-form or interval — regenerate the schedule block with the
446
+ // same count/cadence, shifted to start at HH:MM.
447
+ const { count, cadenceMin } = cadenceFromUnit(xml);
448
+ const indent = detectIndent(xml);
449
+ const i2 = indent + indent;
450
+
451
+ let block;
452
+ if (count <= 1 || cadenceMin == null) {
453
+ block =
454
+ `${indent}<key>StartCalendarInterval</key>\n` +
455
+ `${indent}<dict>\n` +
456
+ `${i2}<key>Hour</key>\n` +
457
+ `${i2}<integer>${h}</integer>\n` +
458
+ `${i2}<key>Minute</key>\n` +
459
+ `${i2}<integer>${m}</integer>\n` +
460
+ `${indent}</dict>\n`;
461
+ } else {
462
+ const startMin = h * 60 + m;
463
+ const lines = [];
464
+ for (let i = 0; i < count; i++) {
465
+ const t = (startMin + i * cadenceMin) % 1440;
466
+ const hh = Math.floor(t / 60);
467
+ const mm = t % 60;
468
+ lines.push(`${i2}<dict><key>Hour</key><integer>${hh}</integer><key>Minute</key><integer>${mm}</integer></dict>`);
469
+ }
470
+ block =
471
+ `${indent}<key>StartCalendarInterval</key>\n` +
472
+ `${indent}<array>\n` +
473
+ lines.join('\n') + '\n' +
474
+ `${indent}</array>\n`;
475
+ }
476
+
477
+ let next = xml.replace(
478
+ /[ \t]*<key>StartInterval<\/key>[ \t\r\n]*<integer>\d+<\/integer>\n?/,
479
+ ''
480
+ );
481
+ next = next.replace(
482
+ /[ \t]*<key>StartCalendarInterval<\/key>[ \t\r\n]*(?:<dict>[\s\S]*?<\/dict>|<array>[\s\S]*?<\/array>)\n?/,
483
+ ''
484
+ );
485
+ const idx = next.lastIndexOf('</dict>');
486
+ if (idx === -1) return { ok: false, reason: 'malformed plist' };
487
+ next = next.slice(0, idx) + block + next.slice(idx);
488
+ fs.writeFileSync(unitPath, next);
489
+ return { ok: true, kind: count > 1 ? 'array' : 'single', count };
490
+ }
491
+
492
+ function detectIndent(xml) {
493
+ const spaceIndented = /^ <key>/m.test(xml) && !/^\t<key>/m.test(xml);
494
+ return spaceIndented ? ' ' : '\t';
495
+ }
496
+
497
+ module.exports = {
498
+ renderPlist,
499
+ generate,
500
+ defaultEnv,
501
+ list,
502
+ isLoaded,
503
+ pidFor,
504
+ load,
505
+ unload,
506
+ kickstart,
507
+ killJob,
508
+ install,
509
+ unitFileName,
510
+ discoverJobs,
511
+ parseUnit,
512
+ scheduleFromUnit,
513
+ nextRunFromUnit,
514
+ updateInterval,
515
+ startTimeFromUnit,
516
+ updateStartTime,
517
+ cadenceFromUnit,
518
+ };
@@ -0,0 +1,68 @@
1
+ {
2
+ "mcpServers": {
3
+ "reddit-agent": {
4
+ "type": "stdio",
5
+ "command": "/usr/bin/python3",
6
+ "args": [
7
+ "__HOME__/social-autoposter/scripts/mcp_lock_proxy.py",
8
+ "--lock-name",
9
+ "reddit-browser",
10
+ "--ttl",
11
+ "90",
12
+ "--",
13
+ "npx",
14
+ "@playwright/mcp@latest",
15
+ "--config",
16
+ "__HOME__/.claude/browser-agent-configs/reddit-agent.json"
17
+ ],
18
+ "env": {
19
+ "PATH": "__NODE_BIN__:/usr/bin:/bin",
20
+ "BROWSER_LOCK_NAME": "reddit-browser",
21
+ "BROWSER_LOCK_TTL": "90"
22
+ }
23
+ },
24
+ "linkedin-agent": {
25
+ "type": "stdio",
26
+ "command": "npx",
27
+ "args": [
28
+ "@playwright/mcp@latest",
29
+ "--config",
30
+ "__HOME__/.claude/browser-agent-configs/linkedin-agent.json"
31
+ ],
32
+ "env": {
33
+ "PATH": "__NODE_BIN__:/usr/bin:/bin"
34
+ }
35
+ },
36
+ "twitter-harness": {
37
+ "type": "stdio",
38
+ "command": "__UV_BIN__",
39
+ "args": [
40
+ "run",
41
+ "--quiet",
42
+ "--with",
43
+ "mcp",
44
+ "__HOME__/.claude/mcp-servers/browser-harness/server.py"
45
+ ],
46
+ "env": {
47
+ "PATH": "__HOME__/.local/bin:__NODE_BIN__:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
48
+ }
49
+ },
50
+ "linkedin-harness": {
51
+ "type": "stdio",
52
+ "command": "__HOME__/.local/bin/uv",
53
+ "args": [
54
+ "run",
55
+ "--quiet",
56
+ "--with",
57
+ "mcp",
58
+ "__HOME__/.claude/mcp-servers/browser-harness/server.py"
59
+ ],
60
+ "env": {
61
+ "PATH": "__HOME__/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin",
62
+ "BU_NAME": "linkedin-harness",
63
+ "BH_PORT": "9556",
64
+ "BH_PROFILE_NAME": "browser-harness-linkedin"
65
+ }
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "mcpServers": {
3
+ "linkedin-agent": {
4
+ "type": "stdio",
5
+ "command": "npx",
6
+ "args": [
7
+ "@playwright/mcp@latest",
8
+ "--config",
9
+ "__HOME__/.claude/browser-agent-configs/linkedin-agent.json"
10
+ ],
11
+ "env": {
12
+ "PATH": "__NODE_BIN__:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
13
+ }
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "browser": {
3
+ "userDataDir": "__HOME__/.claude/browser-profiles/linkedin",
4
+ "launchOptions": {
5
+ "args": [
6
+ "--window-position=200,200",
7
+ "--window-size=911,1016"
8
+ ]
9
+ },
10
+ "contextOptions": {
11
+ "viewport": { "width": 911, "height": 1016 }
12
+ }
13
+ },
14
+ "outputMode": "file",
15
+ "imageResponses": "omit",
16
+ "outputDir": "__HOME__/.playwright-mcp/linkedin-agent"
17
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "mcpServers": {
3
+ "linkedin-harness": {
4
+ "type": "stdio",
5
+ "command": "__HOME__/.local/bin/uv",
6
+ "args": [
7
+ "run",
8
+ "--quiet",
9
+ "--with",
10
+ "mcp",
11
+ "__HOME__/.claude/mcp-servers/browser-harness/server.py"
12
+ ],
13
+ "env": {
14
+ "PATH": "__HOME__/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin",
15
+ "BU_NAME": "linkedin-harness",
16
+ "BH_PORT": "9556",
17
+ "BH_PROFILE_NAME": "browser-harness-linkedin"
18
+ }
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "mcpServers": {
3
+ "reddit-agent": {
4
+ "type": "stdio",
5
+ "command": "npx",
6
+ "args": [
7
+ "@playwright/mcp@latest",
8
+ "--config",
9
+ "__HOME__/.claude/browser-agent-configs/reddit-agent.json"
10
+ ],
11
+ "env": {
12
+ "PATH": "__NODE_BIN__:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
13
+ }
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "browser": {
3
+ "userDataDir": "__HOME__/.claude/browser-profiles/reddit",
4
+ "launchOptions": {
5
+ "args": [
6
+ "--window-position=150,150",
7
+ "--window-size=911,1016"
8
+ ]
9
+ },
10
+ "contextOptions": {
11
+ "viewport": { "width": 911, "height": 1016 }
12
+ }
13
+ },
14
+ "outputMode": "file",
15
+ "imageResponses": "omit",
16
+ "outputDir": "__HOME__/.playwright-mcp/reddit-agent"
17
+ }