social-autoposter 1.6.50 → 1.6.52
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/mcp/dist/index.js +48 -10
- package/mcp-servers/browser-harness/server.py +23 -2
- package/package.json +1 -1
- package/scripts/engagement_styles.py +50 -30
- package/scripts/ig_post_type_picker.py +15 -0
- package/scripts/linkedin_killswitch.py +559 -7
- package/scripts/sync_ig_to_posts.py +23 -1
- package/skill/lib/twitter-backend.sh +1 -1
- package/skill/linkedin-recovery.sh +247 -36
- package/skill/run-instagram-render.sh +37 -7
package/mcp/dist/index.js
CHANGED
|
@@ -309,10 +309,24 @@ function renderDraftsTable(plan) {
|
|
|
309
309
|
const author = c.thread_author ? `@${c.thread_author}` : "(unknown thread)";
|
|
310
310
|
const style = c.engagement_style ?? "?";
|
|
311
311
|
const reply = c.reply_text ?? "(empty)";
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
312
|
+
// The literal tail URL is NOT known yet: at post time a short link is minted
|
|
313
|
+
// from this target (e.g. fazm.ai/cc -> s4l.ai/r/<code>). Approved drafts
|
|
314
|
+
// always carry the link (post_drafts forces TWITTER_TAIL_LINK_RATE=1.0), so
|
|
315
|
+
// this is the target that WILL be appended. Show the TARGET only; never
|
|
316
|
+
// pre-mint the real /r/ code (that would waste pool codes / split clicks).
|
|
317
|
+
const link = c.link_url
|
|
318
|
+
? `\n + link (appended as a short link at post time): ${c.link_url}`
|
|
319
|
+
: "";
|
|
320
|
+
// The original tweet we're replying to — context the reviewer needs to judge
|
|
321
|
+
// the draft. Already in the plan; just surface it.
|
|
322
|
+
const threadText = c.thread_text
|
|
323
|
+
? `\n in reply to: ${c.thread_text.replace(/\s+/g, " ").trim().slice(0, 280)}`
|
|
324
|
+
: "";
|
|
325
|
+
return (`[${n}] ${author} (style: ${style})` +
|
|
326
|
+
`${threadText}\n` +
|
|
327
|
+
` draft: ${reply.replace(/\n/g, "\n ")}` +
|
|
328
|
+
`${link}\n` +
|
|
329
|
+
` thread url: ${c.candidate_url ?? "?"}`);
|
|
316
330
|
})
|
|
317
331
|
.join("\n\n");
|
|
318
332
|
}
|
|
@@ -331,6 +345,16 @@ async function postApproved(batchId, plan) {
|
|
|
331
345
|
timeoutMs: 900_000,
|
|
332
346
|
env: {
|
|
333
347
|
SAPS_SKIP_CAMPAIGN_SUFFIX: "1",
|
|
348
|
+
// Manual approval is an EXCEPTION to the tail-link A/B. The cron pipeline
|
|
349
|
+
// runs TWITTER_TAIL_LINK_RATE=0.9 (from .env) so ~10% of autopilot posts
|
|
350
|
+
// ship link-less as an experiment arm. But when the user hand-reviews a
|
|
351
|
+
// draft, sees the link target in the table, and approves it, dropping the
|
|
352
|
+
// link is surprising and unwanted. Force 1.0 here so every approved draft
|
|
353
|
+
// carries its link. This wins over .env / process.env because run() spreads
|
|
354
|
+
// opts.env AFTER process.env, and twitter_post_plan.py never load_dotenv's
|
|
355
|
+
// with override, so nothing clobbers it. Cron is untouched (it never goes
|
|
356
|
+
// through this MCP path), so the 0.9 experiment keeps running there.
|
|
357
|
+
TWITTER_TAIL_LINK_RATE: "1.0",
|
|
334
358
|
// The poster attaches to the twitter-harness Chrome over CDP. The cron
|
|
335
359
|
// pipeline exports this from skill/lib/twitter-backend.sh; the MCP path
|
|
336
360
|
// must set it explicitly or twitter_browser.py fails with "No twitter-
|
|
@@ -582,7 +606,9 @@ server.registerTool("draft_cycle", {
|
|
|
582
606
|
description: "Scan X and draft replies on this machine, then return ALL drafts as a numbered table " +
|
|
583
607
|
"for review. This tool POSTS NOTHING. Show the table to the user and ask which numbers " +
|
|
584
608
|
"to post and which to rewrite, then call `post_drafts` with their decision and the " +
|
|
585
|
-
"returned batch_id.
|
|
609
|
+
"returned batch_id. The table MUST show, per draft: the thread being replied to " +
|
|
610
|
+
"(thread_text), the draft reply, and the link target (link_url) when present; never " +
|
|
611
|
+
"drop those columns. Flow: discover -> draft -> review in chat -> post_drafts.",
|
|
586
612
|
inputSchema: {
|
|
587
613
|
project: z
|
|
588
614
|
.string()
|
|
@@ -639,11 +665,15 @@ server.registerTool("draft_cycle", {
|
|
|
639
665
|
const message = `Drafted ${count} ${count === 1 ? "reply" : "replies"} for "${proj}" ` +
|
|
640
666
|
`(batch ${drafted.batchId}). NOTHING has been posted yet.\n\n` +
|
|
641
667
|
`${table}\n\n` +
|
|
642
|
-
`Show this list to the user and ask which to post and which to edit.
|
|
643
|
-
`
|
|
644
|
-
`
|
|
645
|
-
`
|
|
646
|
-
`
|
|
668
|
+
`Show this list to the user and ask which to post and which to edit. When you render ` +
|
|
669
|
+
`the table, ALWAYS include, for every draft: the thread it replies to (in reply to / ` +
|
|
670
|
+
`thread_text), the draft text, and the link target (link_url) if present. Do NOT drop ` +
|
|
671
|
+
`the thread or link columns. Note the literal short link is appended at post time and ` +
|
|
672
|
+
`an A/B gate may omit it, so present link_url as the target, not a guaranteed final URL. ` +
|
|
673
|
+
`They can reply however is natural, e.g. "post 1, 3 and 5", "edit 2: <new wording>", ` +
|
|
674
|
+
`"post all", or "skip all". Editing a draft also posts it. Then call the post_drafts ` +
|
|
675
|
+
`tool with batch_id "${drafted.batchId}" and their decision (post: [numbers], ` +
|
|
676
|
+
`edits: [{n, text}], or post_all: true). Do not post anything the user didn't ask for.`;
|
|
647
677
|
return {
|
|
648
678
|
content: [{ type: "text", text: message }],
|
|
649
679
|
structuredContent: {
|
|
@@ -659,7 +689,15 @@ server.registerTool("draft_cycle", {
|
|
|
659
689
|
n: i + 1,
|
|
660
690
|
author: c.thread_author,
|
|
661
691
|
tweet_url: c.candidate_url,
|
|
692
|
+
// The original tweet being replied to — reviewer context. Hosts that
|
|
693
|
+
// surface ONLY structuredContent (Claude Desktop, Fazm Code tab) need
|
|
694
|
+
// this here or the relayed table loses the thread it's responding to.
|
|
695
|
+
thread_text: c.thread_text,
|
|
662
696
|
reply_text: c.reply_text,
|
|
697
|
+
// Target link only. The literal /r/<code> short link is minted at post
|
|
698
|
+
// time and an A/B gate may omit it; do not pre-mint here.
|
|
699
|
+
link_url: c.link_url,
|
|
700
|
+
style: c.engagement_style,
|
|
663
701
|
language: c.language,
|
|
664
702
|
})),
|
|
665
703
|
},
|
|
@@ -249,11 +249,32 @@ def _build_chrome_cmd() -> list[str]:
|
|
|
249
249
|
cmd.append(f"--window-position={win_pos}")
|
|
250
250
|
cmd.append(f"--window-size={win_size}")
|
|
251
251
|
|
|
252
|
-
# Open a
|
|
253
|
-
|
|
252
|
+
# Open a REAL http(s) page (NOT about:blank) so the harness daemon's
|
|
253
|
+
# attach_first_page() finds an existing real tab and reuses it. Upstream
|
|
254
|
+
# browser-harness creates a throwaway about:blank on every re-attach where
|
|
255
|
+
# is_real_page() is false (which about:blank is); launching blank feeds that
|
|
256
|
+
# loop, piling up orphan tabs that cleanup_harness_tabs then has to sweep.
|
|
257
|
+
# Landing URL is derived from the profile's platform; BH_LAUNCH_URL overrides.
|
|
258
|
+
cmd.append(_launch_url())
|
|
254
259
|
return cmd
|
|
255
260
|
|
|
256
261
|
|
|
262
|
+
def _launch_url() -> str:
|
|
263
|
+
"""Initial tab URL for the managed Chrome. MUST be a real http(s) page, not
|
|
264
|
+
about:blank, so the browser-harness daemon reuses it instead of spawning
|
|
265
|
+
throwaway blank tabs (see _build_chrome_cmd). Derived from PROFILE_NAME;
|
|
266
|
+
BH_LAUNCH_URL overrides for non-standard profiles."""
|
|
267
|
+
override = os.environ.get("BH_LAUNCH_URL", "").strip()
|
|
268
|
+
if override:
|
|
269
|
+
return override
|
|
270
|
+
name = PROFILE_NAME.lower()
|
|
271
|
+
if "linkedin" in name:
|
|
272
|
+
return "https://www.linkedin.com/feed/"
|
|
273
|
+
if "reddit" in name:
|
|
274
|
+
return "https://www.reddit.com/"
|
|
275
|
+
return "https://x.com"
|
|
276
|
+
|
|
277
|
+
|
|
257
278
|
def ensure_chrome() -> dict:
|
|
258
279
|
"""Make sure our managed Chrome is running on PORT. Idempotent."""
|
|
259
280
|
if _cdp_alive():
|
package/package.json
CHANGED
|
@@ -182,20 +182,24 @@ STYLES = {
|
|
|
182
182
|
},
|
|
183
183
|
"ig_walkin_storefront_playbook": {
|
|
184
184
|
"description": (
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
"
|
|
185
|
+
"Product demo for mk0r: show mk0r building a REAL website for a "
|
|
186
|
+
"local business that has none. 'i noticed [a local spot] near me "
|
|
187
|
+
"with no website, just a maps pin and one blurry photo. i opened "
|
|
188
|
+
"mk0r.com, described the place in one prompt, and watched it build "
|
|
189
|
+
"a real site, hero, services, hours, a call button, live in "
|
|
190
|
+
"minutes.' Focus on the CAPABILITY and the speed, and what it "
|
|
191
|
+
"means for a small business to finally have a real site online. NO "
|
|
192
|
+
"earnings, NO 'they paid me', NO recurring-revenue or 'signed N "
|
|
193
|
+
"clients' totals. Reference mk0r.com plainly."
|
|
192
194
|
),
|
|
193
195
|
"example": (
|
|
194
|
-
"
|
|
195
|
-
"
|
|
196
|
-
"
|
|
197
|
-
"
|
|
198
|
-
"
|
|
196
|
+
"there's a tire shop two blocks from me thats been open fifteen "
|
|
197
|
+
"years. i went to send a friend the link and there was no link, "
|
|
198
|
+
"just a maps pin and one blurry photo someone else uploaded. so i "
|
|
199
|
+
"opened mk0r.com and described the shop in one prompt, brakes, "
|
|
200
|
+
"tires, the hours, the phone number. it built the whole site while "
|
|
201
|
+
"i watched, a hero, a services list, a map, one big call button. "
|
|
202
|
+
"fifteen years open and it finally has a front door online. mk0r.com"
|
|
199
203
|
),
|
|
200
204
|
"best_in": {
|
|
201
205
|
"instagram": [
|
|
@@ -204,29 +208,40 @@ STYLES = {
|
|
|
204
208
|
],
|
|
205
209
|
},
|
|
206
210
|
"note": (
|
|
207
|
-
"Caption is the
|
|
208
|
-
"
|
|
209
|
-
"
|
|
210
|
-
"'
|
|
211
|
-
"
|
|
211
|
+
"Caption is the product-demo arc: a real local business with no "
|
|
212
|
+
"website, mk0r builds a real site from one prompt, what it means "
|
|
213
|
+
"for the owner to finally be online. NO income/earnings/'they paid "
|
|
214
|
+
"me'/recurring-revenue/'signed N clients' framing -- that arc "
|
|
215
|
+
"tripped a Meta fraud-and-deceptive-practices restriction on "
|
|
216
|
+
"2026-06-02 (matt_diak link-sharing restricted 30d). Reference "
|
|
217
|
+
"'mk0r.com' plainly in the caption. project_name='mk0r' on the "
|
|
218
|
+
"row. Fires when TARGET=product AND selected_project=mk0r."
|
|
212
219
|
),
|
|
213
220
|
"target_chars": 1800,
|
|
214
221
|
},
|
|
215
222
|
"ig_studyly_failing_student_arc": {
|
|
216
223
|
"description": (
|
|
217
|
-
"
|
|
218
|
-
"
|
|
219
|
-
"
|
|
220
|
-
"my [notes/chapter] in
|
|
221
|
-
"
|
|
222
|
-
"
|
|
224
|
+
"Study-method arc for studyly: 'i was [age], [program]. i was "
|
|
225
|
+
"[rereading/highlighting/making flashcards] for hours and still "
|
|
226
|
+
"blanking the moment the wording changed. a friend sent me "
|
|
227
|
+
"studyly.io. i pasted my [notes/chapter] in and it quizzed me on "
|
|
228
|
+
"my own material until i could answer without looking, rewording "
|
|
229
|
+
"each question so i couldnt pattern-match. for the first time i "
|
|
230
|
+
"could tell what i actually knew from what i only recognized.' "
|
|
231
|
+
"Focus on the METHOD shift (active recall vs rereading, testing on "
|
|
232
|
+
"your own deck). Closes with the rereading-is-theater lesson + "
|
|
233
|
+
"studyly.io footer. Do NOT promise or fabricate specific "
|
|
234
|
+
"before/after exam scores as a typical result."
|
|
223
235
|
),
|
|
224
236
|
"example": (
|
|
225
|
-
"i was 19. premed
|
|
226
|
-
"
|
|
227
|
-
"
|
|
228
|
-
"
|
|
229
|
-
"
|
|
237
|
+
"i was 19. premed, third semester, organic chemistry. i had reread "
|
|
238
|
+
"the chapter four times and could still blank on a mechanism the "
|
|
239
|
+
"second the wording changed. i pasted my notes into studyly.io at "
|
|
240
|
+
"2am and it asked me the things i thought i knew, rewording each "
|
|
241
|
+
"one so i couldnt coast on the first three words. that was the "
|
|
242
|
+
"first night i could actually tell what i knew from what i only "
|
|
243
|
+
"recognized. the lesson is rereading is recognizing. close the "
|
|
244
|
+
"book. let something ask you. studyly.io"
|
|
230
245
|
),
|
|
231
246
|
"best_in": {
|
|
232
247
|
"instagram": [
|
|
@@ -237,8 +252,13 @@ STYLES = {
|
|
|
237
252
|
"note": (
|
|
238
253
|
"Caption is shorter than mk0r (1400-1900 chars). 'here is a story.' "
|
|
239
254
|
"opener optional. MUST include 'studyly.io' footer. "
|
|
240
|
-
"project_name='studyly'. Lesson is always rereading-is-theater. "
|
|
241
|
-
"
|
|
255
|
+
"project_name='studyly'. Lesson is always rereading-is-theater. NO "
|
|
256
|
+
"specific before/after exam scores or 'failed -> passed/topped the "
|
|
257
|
+
"class' miracle jumps presented as a typical/guaranteed outcome -- "
|
|
258
|
+
"exaggerated-results claims are a deceptive-practices signal (same "
|
|
259
|
+
"Meta rail that restricted mk0r 2026-06-02); keep outcomes "
|
|
260
|
+
"qualitative and personal. Fires when TARGET=product AND "
|
|
261
|
+
"selected_project=studyly."
|
|
242
262
|
),
|
|
243
263
|
"target_chars": 1650,
|
|
244
264
|
},
|
|
@@ -159,6 +159,21 @@ def main():
|
|
|
159
159
|
# product->organic fallback this preserves cadence without weakening
|
|
160
160
|
# the cooldown.
|
|
161
161
|
other = "product" if target == "organic" else "organic"
|
|
162
|
+
# ...but ONLY if the other type is still eligible (config weight > 0).
|
|
163
|
+
# A type explicitly disabled via post_type_weights=0 (e.g. product
|
|
164
|
+
# paused during an IG link-sharing restriction) must NEVER be posted
|
|
165
|
+
# through the empty-queue fallback, otherwise weight=0 is not a real
|
|
166
|
+
# off switch. If the only eligible type's queue is empty, exit 2
|
|
167
|
+
# (nothing to post this fire) rather than leak the disabled type.
|
|
168
|
+
if other not in eligible:
|
|
169
|
+
sys.stderr.write(
|
|
170
|
+
f"queue empty for eligible type '{target}'; fallback type "
|
|
171
|
+
f"'{other}' is disabled (config_weights={type_weights_cfg}), "
|
|
172
|
+
"not leaking it"
|
|
173
|
+
+ (f" (target_account={account})" if account else "")
|
|
174
|
+
+ "\n"
|
|
175
|
+
)
|
|
176
|
+
sys.exit(2)
|
|
162
177
|
if other == "product":
|
|
163
178
|
row = _pick_product_with_cooldown()
|
|
164
179
|
else:
|