social-autoposter 1.6.185 → 1.6.186

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 CHANGED
@@ -1092,11 +1092,13 @@ tool("engagement_mode", {
1092
1092
  description: "Set or read the engagement LANES the autopilot drafts in. There are TWO independent lanes that " +
1093
1093
  "can BOTH be on (the cycle then splits 50/50): PERSONAL BRAND (organic, link-free engagement in " +
1094
1094
  "the user's own voice — ON by default) and PRODUCT PROMOTION (the marketing pipeline, link " +
1095
- "replies — OFF by default, opt-in). This is a SETUP step: AFTER X is connected and the profile " +
1096
- "is scanned, personal-brand is already the default, so ASK the user the ONE question: do they " +
1095
+ "replies — OFF by default, opt-in). This is a SETUP step: AFTER X is connected, the profile " +
1096
+ "is scanned (for VOICE), and the user has answered the DICTATION interview (for TOPICS + corpus), " +
1097
+ "personal-brand is already the default, so ASK the user the ONE question: do they " +
1097
1098
  "ALSO want to promote a product? Then call action:'set' with personal_brand:true and " +
1098
- "promotion:true|false. Pass the voice/description/topics you extracted from the profile scan so " +
1099
- "the persona is grounded in who they actually are, AND expand those topics into a search_queries " +
1099
+ "promotion:true|false. Pass the voice/description you captured from the scan, the search_topics " +
1100
+ "you extracted PRIMARILY from the dictation, and the raw dictation transcript as content_corpus, " +
1101
+ "so the persona is grounded in who they actually are, AND expand those topics into a search_queries " +
1100
1102
  "array of ~30 concrete X advanced-search strings in the SAME call (identical to project_config) " +
1101
1103
  "so the personal-brand cycle has a real query bank on day one instead of running one crude " +
1102
1104
  "topic-as-query. If they want promotion too, continue to configure the product project with " +
@@ -1130,15 +1132,22 @@ tool("engagement_mode", {
1130
1132
  content_angle: z
1131
1133
  .string()
1132
1134
  .optional()
1133
- .describe("Persona grounding: a paragraph of concrete first-hand experience the persona speaks from."),
1135
+ .describe("Persona grounding: a paragraph of concrete first-hand experience the persona speaks from, synthesized from the DICTATION interview (contrarian takes, earned expertise) with the scan as backup."),
1136
+ content_corpus: z
1137
+ .string()
1138
+ .optional()
1139
+ .describe("The RAW voice-memo transcript from the onboarding dictation interview, VERBATIM (do NOT " +
1140
+ "paraphrase or summarize). Persisted to the persona_corpus.txt sidecar (never config.json), " +
1141
+ "capped ~8000 chars. This is the grounding pool the drafter quotes real specifics from " +
1142
+ "(actual projects, numbers, opinions, phrasing), so keep it dense and first-hand."),
1134
1143
  voice: z
1135
1144
  .any()
1136
1145
  .optional()
1137
- .describe("Persona voice object {tone, never:[...]} captured from how they actually write."),
1146
+ .describe("Persona voice object {tone, never:[...]} captured from how they actually write (the profile scan) and calibrated by the dictation (who they like/hate reading, phrases they overuse, off-limits)."),
1138
1147
  search_topics: z
1139
1148
  .union([z.array(z.string()), z.string()])
1140
1149
  .optional()
1141
- .describe("~15 topics the persona has genuine experience with, drawn from the scan."),
1150
+ .describe("~15 topics the persona has genuine experience with. Sourced PRIMARILY from the DICTATION interview (the 'subjects you could talk about for an hour' answer), with recurring themes from the profile scan as reinforcement. This is the ONLY field that changes what gets SCANNED on X, so it must reflect what the user WANTS to be in conversations about, not just what they already posted."),
1142
1151
  search_queries: z
1143
1152
  .union([z.array(z.string()), z.string()])
1144
1153
  .optional()
@@ -1217,6 +1226,7 @@ tool("engagement_mode", {
1217
1226
  content_angle: args.content_angle,
1218
1227
  voice: args.voice,
1219
1228
  search_topics: args.search_topics,
1229
+ content_corpus: args.content_corpus,
1220
1230
  });
1221
1231
  personaName = r.name;
1222
1232
  personaCreated = r.created;
@@ -1556,17 +1566,43 @@ tool("project_config", {
1556
1566
  grounding_instructions: scan.grounding_instructions,
1557
1567
  website_research_instructions: WEBSITE_RESEARCH_INSTRUCTIONS,
1558
1568
  onboarding: onboardingSnapshot(),
1559
- next_step: "THREE steps, in order. FIRST (voice, from this scan): read the bio, posts, and comments " +
1569
+ next_step: "FOUR steps, in order. FIRST (VOICE, from this scan): read the bio, posts, and comments " +
1560
1570
  "as GROUND TRUTH and, per grounding_instructions, extract their profession/identity, " +
1561
1571
  "voice & vibe (tone, phrasing, casing, tics), 2-4 verbatim golden-rule example replies, " +
1562
- "a phrase bank + things they avoid, their icp, and recurring themes -> search_topics. " +
1563
- "SECOND (engagement lanes ASK THE USER, do not infer): the PERSONAL BRAND lane (organic, " +
1572
+ "a phrase bank + things they avoid, and their icp. The scan is BACKWARD-LOOKING (only what " +
1573
+ "they already posted) so it is the source for VOICE, not the primary source for topics. " +
1574
+ "SECOND (the DICTATION interview — this is where TOPICS + grounding corpus come from, do NOT " +
1575
+ "skip it and do NOT infer topics from the scan alone): tell the user to answer ALL of the " +
1576
+ "following in ONE spoken dictation (the Claude input box already supports dictation, so they " +
1577
+ "just talk once and you split the answers into fields). Ask verbatim, as a single numbered " +
1578
+ "list:\n" +
1579
+ " 1. Who are you, and what do you want to be known for? (-> description)\n" +
1580
+ " 2. What subjects could you talk about for an hour, work and non-work? (-> search_topics: " +
1581
+ "this is the LOAD-BEARING answer, it is the ONLY thing that decides what gets scanned on X, " +
1582
+ "so it must capture what they WANT to be in conversations about)\n" +
1583
+ " 3. Your most contrarian takes — what does everyone in your field get wrong, and what did " +
1584
+ "you used to believe that you have reversed on? (-> content_angle + corpus)\n" +
1585
+ " 4. What can you explain in 5 minutes that took you years, and what mistake do you watch " +
1586
+ "beginners make over and over? (-> content_angle + corpus)\n" +
1587
+ " 5. Best or worst thing that happened to you recently, and a failure you learned the most " +
1588
+ "from? (-> corpus, keeps drafts current)\n" +
1589
+ " 6. Who do you love or hate reading online, and any lines or phrases you say a lot? " +
1590
+ "(-> voice calibration)\n" +
1591
+ " 7. Anything off-limits (topics, companies, people), and how spicy can we get — safe, " +
1592
+ "opinionated, or provocative? (-> content_guardrails + voice.never)\n" +
1593
+ "Then SYNTHESIZE the fields from their dictation: search_topics comes PRIMARILY from answer 2 " +
1594
+ "(fold in recurring scan themes only as reinforcement); description/content_angle/voice from " +
1595
+ "the rest. Keep their RAW transcript VERBATIM as content_corpus (do NOT paraphrase; their " +
1596
+ "actual numbers, opinions, and phrasing are what make drafts sound like them). If the user " +
1597
+ "declines or gives nothing usable, fall back to scan-derived topics. " +
1598
+ "THIRD (engagement lanes — ASK THE USER, do not infer): the PERSONAL BRAND lane (organic, " +
1564
1599
  "link-free engagement in their own voice) is ON by default, so ask the ONE question — do they " +
1565
1600
  "ALSO want to PROMOTE a PRODUCT (the marketing lane, link replies)? Both lanes can run (the " +
1566
1601
  "cycle splits 50/50). Call the `engagement_mode` tool action:'set' with personal_brand:true, " +
1567
- "promotion:true|false AND the voice/description/topics you just extracted (this provisions a " +
1568
- "persona grounded in this scan). " +
1569
- "THIRD (product, ONLY if they wanted promotion): follow " +
1602
+ "promotion:true|false AND the voice/description/search_topics you synthesized PLUS the raw " +
1603
+ "dictation transcript as content_corpus (this provisions the persona and seeds topics). Only " +
1604
+ "NOW are topics seeded postponed until the dictation is in. " +
1605
+ "FOURTH (product, ONLY if they wanted promotion): follow " +
1570
1606
  "website_research_instructions — discover the product URL from config, context, profile " +
1571
1607
  "links/posts, or public research and read 5+ of its pages to fill description, " +
1572
1608
  "differentiator, icp, get_started_link, and content_guardrails, written in the voice you " +
package/mcp/dist/setup.js CHANGED
@@ -383,6 +383,20 @@ export function ensurePersonaProject(grounding) {
383
383
  }
384
384
  fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
385
385
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
386
+ // Raw dictation transcript -> persona_corpus.txt sidecar (NOT config.json).
387
+ // Mirrors scripts/build_persona.py cmd_apply: config.json is inlined into many
388
+ // prompts (ALL_PROJECTS_JSON), so a multi-KB corpus there would bloat every
389
+ // cycle's token bill. The persona lane reads this file only when it drafts.
390
+ // Best-effort; a write failure must never block persona provisioning.
391
+ if (grounding && typeof grounding.content_corpus === "string" && grounding.content_corpus.trim()) {
392
+ try {
393
+ const corpusPath = path.join(path.dirname(cfgPath), "persona_corpus.txt");
394
+ fs.writeFileSync(corpusPath, grounding.content_corpus.trim().slice(0, 8000) + "\n", "utf-8");
395
+ }
396
+ catch {
397
+ // ignore: corpus is grounding fuel, not required for a working persona.
398
+ }
399
+ }
386
400
  return { name, created };
387
401
  }
388
402
  // Heal installs that onboarded BEFORE short_links_live defaulted to false.
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.6.185",
3
- "installedAt": "2026-07-01T21:48:09.463Z"
2
+ "version": "1.6.186",
3
+ "installedAt": "2026-07-01T22:02:36.862Z"
4
4
  }
package/mcp/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "dxt_version": "0.1",
3
3
  "name": "social-autoposter",
4
4
  "display_name": "S4L",
5
- "version": "1.6.185",
5
+ "version": "1.6.186",
6
6
  "description": "Draft, review, approve, and autopilot X/Twitter posts.",
7
7
  "long_description": "## **⚠️ The disclaimer above is generic Claude boilerplate.** Anthropic shows the same warning on every plugin regardless of what it does; any plugin has the same level of access as any app you download from the internet.\n\nS4L is an open source product developed by Mediar.ai Incorporated, a VC-backed San Francisco-based startup.\n\nTo get started:\n\n1\\. Copy this prompt: **Set me up on S4L plugin end to end**\n\n2\\. Quit with CMD+Q, reopen Claude, paste into a new chat.\n\nWhat happens next:\n\n* About every 5 minutes S4L scans X for posts that match your topics and drafts replies in your voice.\n* Drafts show up as review cards, usually the first within a few minutes. Nothing is posted automatically; you approve each one.\n* Posting autopilot stays off until you explicitly turn it on.",
8
8
  "author": {
@@ -315,9 +315,10 @@ class S4LMenuBar(rumps.App):
315
315
  # read per second.
316
316
  self._act_poll = rumps.Timer(self._poll_activity, 1.0)
317
317
  self._act_poll.start()
318
- # Update availability comes from ONE source: the MCP's versionStatus
319
- # (npm + semver `>`), surfaced in the snapshot as update_available/
320
- # latest_version the SAME source the dashboard uses. _tick copies those
318
+ # Update availability comes from ONE source: scripts/snapshot.py's
319
+ # _latest_published (GitHub releases/latest first, npm fallback boxes
320
+ # have no npm; same probe as mcp/src/version.ts), surfaced in the
321
+ # snapshot as update_available/latest_version. _tick copies those
321
322
  # snapshot fields onto these attrs every poll. No second GitHub/manifest
322
323
  # check here anymore (it diverged from the header and once showed an
323
324
  # "update to an OLDER version" because it polled a different registry).
@@ -1431,9 +1432,9 @@ class S4LMenuBar(rumps.App):
1431
1432
  elif not attention:
1432
1433
  self._stall_notified = False
1433
1434
 
1434
- # Single-source update signal: copy the snapshot's versionStatus result
1435
- # (npm + semver >, surfaced as update_available/latest_version) — exactly
1436
- # what the dashboard uses. No separate GitHub/manifest poll anymore.
1435
+ # Single-source update signal: copy the snapshot's result (snapshot.py
1436
+ # _latest_published: GitHub releases/latest first, npm fallback; semver >,
1437
+ # surfaced as update_available/latest_version). No separate poll here.
1437
1438
  self._update_available = bool(snap.get("update_available"))
1438
1439
  self._latest_version = snap.get("latest_version")
1439
1440
 
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/social-autoposter-mcp",
3
- "version": "1.6.185",
3
+ "version": "1.6.186",
4
4
  "private": true,
5
5
  "description": "Desktop MCP client for social-autoposter (X/Twitter rail): manual draft/review/approve loop, autopilot control, and stats. Thin wrapper over the existing pipeline scripts.",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "social-autoposter",
3
- "version": "1.6.185",
3
+ "version": "1.6.186",
4
4
  "description": "Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Install as a Claude Code agent skill.",
5
5
  "bin": {
6
6
  "social-autoposter": "bin/cli.js"
@@ -3,8 +3,8 @@
3
3
 
4
4
  Produces the SAME dict as the MCP's buildSnapshot() (mcp/src/index.ts), but in
5
5
  Python, reading directly from the stateful files plus two existing Python helpers
6
- (setup_twitter_auth.py for X, schedule_state.py for the draft schedule) and npm
7
- for the latest version.
6
+ (setup_twitter_auth.py for X, schedule_state.py for the draft schedule) and
7
+ GitHub releases/latest (npm fallback) for the latest version.
8
8
 
9
9
  WHY this exists: the menu bar must render with Claude / the MCP fully closed. The
10
10
  MCP is a Node process tied to Claude Desktop's lifecycle, and it was the ONLY
@@ -17,8 +17,8 @@ to the whole snapshot. The source of truth is the FILES; this is just the reader
17
17
 
18
18
  PURE READ/COMPUTE: never writes (no onboarding-milestone telemetry, no
19
19
  persistence) — the MCP keeps those side effects around this. Slow fields (X
20
- session, npm latest) are cached per-process with a TTL so a 5s menu-bar tick that
21
- imports this module stays cheap.
20
+ session, latest version) are cached per-process with a TTL so a 5s menu-bar tick
21
+ that imports this module stays cheap.
22
22
  """
23
23
  from __future__ import annotations
24
24
 
@@ -259,10 +259,49 @@ def _x_status():
259
259
  return val
260
260
 
261
261
 
262
- # ---- version (resolveVersion + npm latest + semver), cached ----------------
263
- _ver_cache = {"at": 0.0, "latest": None}
262
+ # ---- version (resolveVersion + latest release + semver), cached ------------
263
+ # Latest-version SOURCE = GitHub releases/latest first, npm only as a fallback.
264
+ # This mirrors mcp/src/version.ts::latestPublishedVersion and is load-bearing:
265
+ # the .mcpb boxes that render the menu bar have NO npm on PATH (PATH is just
266
+ # /usr/bin:/bin:/usr/sbin:/sbin), so an npm-only probe always yields latest=None
267
+ # there, update_available is always False, and the "S4L ⬆" banner can never
268
+ # fire on a box — even with a new release live. The 2026-07-01 v1.6.182 fix
269
+ # closed this in version.ts only; the menu bar computes its snapshot through
270
+ # THIS module (mcp/menubar/s4l_state.py tier 1, the loopback tier was removed
271
+ # to fix a UI freeze), so the same probe must live here too. curl is at
272
+ # /usr/bin/curl on every macOS PATH. GitHub releases/latest is also the SAME
273
+ # source the box updater installs from (s4l_box_update.sh / _mcpb_update_work),
274
+ # so "update available" and "what an update installs" cannot disagree.
275
+ _ver_cache = {"at": 0.0, "latest": None, "checked": False}
264
276
  _VER_TTL = 600.0
265
277
 
278
+ _RELEASES_LATEST_API = "https://api.github.com/repos/m13v/social-autoposter/releases/latest"
279
+
280
+
281
+ def _latest_from_github():
282
+ try:
283
+ # -f: fail on HTTP error, -sSL: quiet + follow redirects, -m: hard timeout.
284
+ # releases/latest already excludes drafts and prereleases.
285
+ res = subprocess.run(
286
+ ["/usr/bin/curl", "-fsSL", "-m", "10",
287
+ "-H", "Accept: application/vnd.github+json", _RELEASES_LATEST_API],
288
+ capture_output=True, text=True, timeout=12)
289
+ tag = (json.loads(res.stdout) or {}).get("tag_name")
290
+ v = tag.lstrip("v").strip() if isinstance(tag, str) else ""
291
+ return v if v and v[0].isdigit() and v.count(".") >= 2 else None
292
+ except Exception:
293
+ return None
294
+
295
+
296
+ def _latest_from_npm():
297
+ try:
298
+ res = subprocess.run(["npm", "view", "social-autoposter", "version"],
299
+ capture_output=True, text=True, timeout=8)
300
+ line = (res.stdout.strip().splitlines() or [""])[-1].strip()
301
+ return line if line and line[0].isdigit() else None
302
+ except Exception:
303
+ return None
304
+
266
305
 
267
306
  def _resolve_version() -> str:
268
307
  for p in (
@@ -278,18 +317,15 @@ def _resolve_version() -> str:
278
317
 
279
318
  def _latest_published():
280
319
  now = time.time()
281
- if _ver_cache["latest"] is not None and now - _ver_cache["at"] < _VER_TTL:
320
+ # Cache failures (latest=None) too, like version.ts: a menu-bar tick loop
321
+ # re-probing an unreachable/rate-limited GitHub every few seconds would burn
322
+ # the unauthenticated API quota (60/h per IP) and lock itself out for good.
323
+ if _ver_cache["checked"] and now - _ver_cache["at"] < _VER_TTL:
282
324
  return _ver_cache["latest"]
283
- latest = None
284
- try:
285
- res = subprocess.run(["npm", "view", "social-autoposter", "version"],
286
- capture_output=True, text=True, timeout=8)
287
- line = (res.stdout.strip().splitlines() or [""])[-1].strip()
288
- if line and line[0].isdigit():
289
- latest = line
290
- except Exception:
291
- latest = None
292
- _ver_cache.update(at=now, latest=latest)
325
+ latest = _latest_from_github()
326
+ if latest is None:
327
+ latest = _latest_from_npm()
328
+ _ver_cache.update(at=now, latest=latest, checked=True)
293
329
  return latest
294
330
 
295
331
 
package/setup/SKILL.md CHANGED
@@ -42,8 +42,12 @@ Do not report setup complete until all of these are true:
42
42
 
43
43
  1. The owned runtime is installed and ready.
44
44
  2. At least one project is ready with name, website, description, ICP, voice,
45
- and search topics.
46
- 3. Search topics have been seeded into the backend.
45
+ and search topics. For the personal-brand persona, the search topics and the
46
+ grounding corpus come from the user's dictation interview (below), not from
47
+ the profile scan alone.
48
+ 3. Search topics have been seeded into the backend. Seeding is POSTPONED until
49
+ after the dictation interview so the topics reflect what the user wants to be
50
+ in conversations about, not only what they already posted.
47
51
  4. X is connected and the real handle has been auto-detected.
48
52
  5. The draft autopilot is scheduled and has produced a draft card without
49
53
  posting. A returned/pending review batch is the strongest success signal. If X
@@ -59,7 +63,10 @@ Do not report setup complete until all of these are true:
59
63
  `DATABASE_URL`.
60
64
  - **Search topics**: the X cycle reads `project_search_topics`, seeded
61
65
  automatically from each project's `search_topics`. No topics means nothing
62
- to scan.
66
+ to scan. `search_topics` is the ONLY field that changes what gets scanned;
67
+ every other persona field only shapes the draft after a thread is found. For
68
+ the persona, the profile scan is backward-looking (only what the user already
69
+ posted), so topics are sourced primarily from the dictation interview.
63
70
 
64
71
  ## Choose the path
65
72
 
@@ -104,17 +111,53 @@ If X is not connected:
104
111
  `connect_x` after sign-in and continue.
105
112
 
106
113
  Once connected, call `project_config` with `action:'profile_scan'`. Treat the returned
107
- bio, links, recent posts, and replies as grounding truth for:
114
+ bio, links, recent posts, and replies as grounding truth for the VOICE, not as the
115
+ primary source of topics (the scan only shows what they already posted):
108
116
 
109
117
  - profession/identity;
110
118
  - voice, casing, phrasing, and tone;
111
119
  - ICP;
112
- - recurring themes and 5-15 literal X search topics;
113
- - wording or claims the user avoids.
120
+ - wording or claims the user avoids;
121
+ - recurring themes (reinforcement for topics, not the origin).
114
122
 
115
123
  Do not ask the user to approve the inferred voice during initial setup. Save a
116
124
  specific, conservative best draft and mention afterward that it can be edited.
117
125
 
126
+ Then run the DICTATION INTERVIEW before extracting topics or seeding. This is
127
+ where the persona's `search_topics` and grounding corpus come from. Tell the user
128
+ to answer all of the following in ONE spoken dictation (the Claude input box
129
+ already supports dictation, so they talk once and you split the answers into
130
+ fields). Ask verbatim as a single numbered list:
131
+
132
+ 1. Who are you, and what do you want to be known for? (→ description)
133
+ 2. What subjects could you talk about for an hour, work and non-work? (→
134
+ `search_topics`; this is the load-bearing answer, it is the only thing that
135
+ decides what gets scanned on X)
136
+ 3. Your most contrarian takes — what does everyone in your field get wrong, and
137
+ what did you used to believe that you have reversed on? (→ content_angle +
138
+ corpus)
139
+ 4. What can you explain in 5 minutes that took you years, and what mistake do you
140
+ watch beginners make over and over? (→ content_angle + corpus)
141
+ 5. Best or worst thing that happened to you recently, and a failure you learned
142
+ the most from? (→ corpus, keeps drafts current)
143
+ 6. Who do you love or hate reading online, and any lines or phrases you say a
144
+ lot? (→ voice calibration)
145
+ 7. Anything off-limits (topics, companies, people), and how spicy can we get —
146
+ safe, opinionated, or provocative? (→ content_guardrails + voice.never)
147
+
148
+ From the dictation, synthesize the fields: `search_topics` primarily from answer
149
+ 2 (fold in recurring scan themes only as reinforcement), the rest from the other
150
+ answers. Keep the RAW transcript VERBATIM as the persona corpus (do not
151
+ paraphrase; the actual numbers, opinions, and phrasing are what make drafts sound
152
+ like them). Pass it as `content_corpus` when you call `engagement_mode` (or
153
+ `project_config`); it is stored in the `persona_corpus.txt` sidecar, not
154
+ config.json. If the user declines or gives nothing usable, fall back to
155
+ scan-derived topics.
156
+
157
+ Only after the dictation is captured do you set the engagement mode / save the
158
+ persona, which seeds the topics. Do not seed topics from the scan before the
159
+ dictation.
160
+
118
161
  ### 3. Discover and research the product
119
162
 
120
163
  Find the product URL in this order:
@@ -195,7 +238,9 @@ turning it into instructions for the user.
195
238
 
196
239
  1. Install/repair: `npx -y social-autoposter@latest init`
197
240
  2. Inspect `~/social-autoposter/config.json` and preserve existing projects.
198
- 3. Discover the product and voice using the same evidence order above.
241
+ 3. Discover the product and voice using the same evidence order above. For a
242
+ personal-brand persona, run the dictation interview (the 7 questions above)
243
+ and take `search_topics` + the raw corpus from it, scan themes as backup.
199
244
  4. Write a complete project with name, website, description, ICP, voice, and
200
245
  `search_topics`.
201
246
  5. Seed topics: