social-autoposter 1.6.94 → 1.6.96

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
@@ -18,7 +18,7 @@ import os from "node:os";
18
18
  import path from "node:path";
19
19
  import fs from "node:fs";
20
20
  import { repoDir, runPython, run, readPlan, writePlan, planPath, scanResultPath, } from "./repo.js";
21
- import { applySetup, resolveProject, listManagedProjectStatus, REQUIRED_FIELDS, RECOMMENDED_FIELDS, configPath, } from "./setup.js";
21
+ import { applySetup, resolveProject, listManagedProjectStatus, ensureShortLinksDefault, REQUIRED_FIELDS, RECOMMENDED_FIELDS, configPath, } from "./setup.js";
22
22
  import { xStatus, xConnect, xDetectSources, xScanProfile, summarizeXAuth } from "./twitterAuth.js";
23
23
  import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, resolveChrome, ensureMenubar, ensurePipelineCurrent, } from "./runtime.js";
24
24
  import { blockOnboardingMilestone, completeOnboardingMilestone, ensureDoctorPhase, onboardingLedger, onboardingSnapshot, recordOnboardingAttempt, runDoctorPhase, } from "./onboarding.js";
@@ -687,6 +687,15 @@ async function postApproved(batchId, plan) {
687
687
  // with override, so nothing clobbers it. Cron is untouched (it never goes
688
688
  // through this MCP path), so the 0.9 experiment keeps running there.
689
689
  TWITTER_TAIL_LINK_RATE: "1.0",
690
+ // Plugin flow only: skip the link_tail Claude call. It just rewords
691
+ // prose around the URL (the minted short link comes from the
692
+ // deterministic wrap step), and on .mcpb boxes there's no `claude`
693
+ // binary so it wastes ~35s/post of run_claude.sh retry backoff before
694
+ // falling back to the mechanical concat anyway. link_tail.py honors
695
+ // this and short-circuits to that concat instantly. The local
696
+ // cron/plist autopilot never sets this, so it keeps generating the
697
+ // bridge sentence.
698
+ SAPS_SKIP_LINK_TAIL: "1",
690
699
  // The poster attaches to the twitter-harness Chrome over CDP. The cron
691
700
  // pipeline exports this from skill/lib/twitter-backend.sh; the MCP path
692
701
  // must set it explicitly or twitter_browser.py fails with "No twitter-
@@ -2987,6 +2996,19 @@ async function main() {
2987
2996
  // scan -> submit path never stalls on an "Ask mode" permission prompt (see the
2988
2997
  // helper's note). Best-effort, allow-only, gated on the task existing.
2989
2998
  ensureAutopilotToolsAllowed();
2999
+ // Heal installs onboarded before short_links_live defaulted to false: such a
3000
+ // project wraps short links against the customer's own domain, which has no
3001
+ // /r/[code] resolver, so every minted link 404s. Re-point them at the s4l.ai
3002
+ // resolver. Idempotent, scoped to managed projects, best-effort.
3003
+ try {
3004
+ const r = ensureShortLinksDefault();
3005
+ if (r.healed.length) {
3006
+ console.error(`[social-autoposter-mcp] short-links heal: routed ${r.healed.join(", ")} through s4l.ai (short_links_live=false)`);
3007
+ }
3008
+ }
3009
+ catch (e) {
3010
+ console.error("[social-autoposter-mcp] short-links heal failed:", e?.message || e);
3011
+ }
2990
3012
  const transport = new StdioServerTransport();
2991
3013
  await server.connect(transport);
2992
3014
  console.error(`[social-autoposter-mcp] connected. v=${VERSION} repo=${repoDir()}`);
package/mcp/dist/setup.js CHANGED
@@ -158,6 +158,16 @@ export function buildProjectEntry(input) {
158
158
  weight: 10,
159
159
  platform: "twitter",
160
160
  voice_relationship: "first_party",
161
+ // A freshly onboarded customer has NOT shipped the @m13v/seo-components
162
+ // /r/[code] resolver on their own domain, so a wrapped short link minted
163
+ // against project.website would 404 ("this link doesn't exist"). Default
164
+ // new projects to route /r/<code> through the social-autoposter-owned
165
+ // resolver at https://s4l.ai instead: short_links_live=false makes
166
+ // _project_short_links_host / getProjectWrapperHost fall back to
167
+ // DEFAULT_FALLBACK_HOST. The customer flips this to true (or removes it)
168
+ // only AFTER they ship their own /r/[code] handler. See the "URL wrapping"
169
+ // section in CLAUDE.md.
170
+ short_links_live: false,
161
171
  ...userFields(input),
162
172
  };
163
173
  // Generic fields can override the defaults above (e.g. platform/weight) and
@@ -211,6 +221,52 @@ export function applySetup(input) {
211
221
  fields_removed,
212
222
  };
213
223
  }
224
+ // Heal installs that onboarded BEFORE short_links_live defaulted to false.
225
+ // Such a project has neither short_links_host nor an explicit short_links_live,
226
+ // so the wrapper host resolves to project.website — but the customer never
227
+ // shipped a /r/[code] resolver there, so every minted short link 404s. Set
228
+ // short_links_live=false (route /r/<code> through s4l.ai) for those projects.
229
+ //
230
+ // Scoped to projects this install actually manages, so a hand-maintained dev
231
+ // config with branded-resolver projects isn't rewritten. Idempotent: a project
232
+ // that already has either short-link field set is left untouched (someone made
233
+ // a deliberate choice). Best-effort; never throws.
234
+ export function ensureShortLinksDefault() {
235
+ const healed = [];
236
+ try {
237
+ const cfg = readConfig();
238
+ const projects = cfg.projects || [];
239
+ if (!projects.length)
240
+ return { healed };
241
+ const managed = new Set(managedProjects());
242
+ let changed = false;
243
+ for (const p of projects) {
244
+ const name = typeof p.name === "string" ? p.name : "";
245
+ if (!name || !managed.has(name))
246
+ continue;
247
+ const hasHost = typeof p.short_links_host === "string" && p.short_links_host.trim() !== "";
248
+ const hasLiveFlag = p.short_links_live === true || p.short_links_live === false;
249
+ if (hasHost || hasLiveFlag)
250
+ continue; // deliberate config: leave it.
251
+ p.short_links_live = false;
252
+ healed.push(name);
253
+ changed = true;
254
+ }
255
+ if (changed) {
256
+ const cfgPath = configPath();
257
+ if (fs.existsSync(cfgPath)) {
258
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
259
+ fs.copyFileSync(cfgPath, `${cfgPath}.bak-${stamp}`);
260
+ }
261
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
262
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
263
+ }
264
+ }
265
+ catch {
266
+ // best-effort heal; a failure here must never block boot.
267
+ }
268
+ return { healed };
269
+ }
214
270
  // ---------------------------------------------------------------------------
215
271
  // Readiness (derived from config.json, never stored).
216
272
  // ---------------------------------------------------------------------------
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.6.94",
3
- "installedAt": "2026-06-24T00:11:08.740Z"
2
+ "version": "1.6.96",
3
+ "installedAt": "2026-06-24T01:22:21.422Z"
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.94",
5
+ "version": "1.6.96",
6
6
  "description": "Draft, review, approve, and autopilot X/Twitter posts. Thin desktop client over the S4L pipeline.",
7
7
  "long_description": "A guided assistant that drafts, reviews, and autopilots X/Twitter posts.\nTo get started:\n1. Click **Configure** and set every tool permission to **Always Allow**.\n2. Copy this prompt: **Set me up on S4L end to end**.\n3. Quit fully with CMD+Q, restart Claude, and paste the prompt into a new chat.",
8
8
  "author": {
@@ -17,6 +17,7 @@ run loop, so that holds).
17
17
  import objc
18
18
  from Foundation import NSObject, NSMakeRect
19
19
  from AppKit import (
20
+ NSApp,
20
21
  NSPanel,
21
22
  NSButton,
22
23
  NSTextField,
@@ -108,8 +109,23 @@ class _ReviewController(NSObject):
108
109
  panel.setDelegate_(self)
109
110
  self._panel = panel
110
111
  self._render()
112
+ # The menu bar app runs as an accessory (LSUIElement, no dock icon), so
113
+ # ordering the panel front is NOT enough to type into the reply field:
114
+ # keystrokes only route to a text view when the OWNING APP is the active
115
+ # application. Without this activation the cursor appears in the field but
116
+ # every keypress goes to whatever app is actually frontmost, which is the
117
+ # "editable field that isn't editable at all" bug. Activate the app, make
118
+ # the panel key, then drop the caret into the reply text view.
119
+ try:
120
+ NSApp.activateIgnoringOtherApps_(True)
121
+ except Exception:
122
+ pass
111
123
  panel.makeKeyAndOrderFront_(None)
112
124
  panel.orderFrontRegardless()
125
+ # _render() already seated the caret in the reply field; re-seat once more
126
+ # after the panel is key so the very first card is editable immediately.
127
+ if self._textview is not None:
128
+ panel.makeFirstResponder_(self._textview)
113
129
 
114
130
  @objc.python_method
115
131
  def _render(self):
@@ -182,6 +198,10 @@ class _ReviewController(NSObject):
182
198
  self._panel.setContentView_(content)
183
199
  # Counter lives in the native title bar, not inside the content.
184
200
  self._panel.setTitle_(f"Review draft {self._idx + 1} of {len(self._drafts)}")
201
+ # setContentView_ rebuilds the view tree, so the caret would otherwise
202
+ # default to the Approve button. Re-seat it in the reply field for every
203
+ # card (not just the first) so each one is immediately editable.
204
+ self._panel.makeFirstResponder_(tv)
185
205
 
186
206
  @objc.python_method
187
207
  def _current_text(self):
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/social-autoposter-mcp",
3
- "version": "1.6.94",
3
+ "version": "1.6.96",
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.94",
3
+ "version": "1.6.96",
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"
@@ -461,6 +461,32 @@ def main() -> int:
461
461
  print(json.dumps(out), flush=True)
462
462
  return 0
463
463
 
464
+ # Plugin (MCP post_drafts) flow sets SAPS_SKIP_LINK_TAIL=1. The bridge only
465
+ # rewords prose around the URL — the minted short link is produced by a
466
+ # separate deterministic wrap step in twitter_post_plan.py — so the Claude
467
+ # call buys nothing there, and on .mcpb customer boxes (no `claude` binary)
468
+ # it burns ~35s of run_claude.sh retry backoff per post before falling back
469
+ # to this exact mechanical concat. Short-circuit straight to the concat.
470
+ # The local cron/plist autopilot leaves this env unset and still generates
471
+ # the bridge sentence.
472
+ if os.environ.get("SAPS_SKIP_LINK_TAIL") == "1":
473
+ limit = TWEET_LIMIT if args.platform == "twitter" else None
474
+ fb_text, fb_trim = enforce_budget(
475
+ mechanical_fallback(reply_text, link_url), link_url,
476
+ limit if limit is not None else TWEET_LIMIT * 100)
477
+ out = {
478
+ "ok": True,
479
+ "text": fb_text,
480
+ "tail": link_url,
481
+ "model_call_ok": False,
482
+ "fallback_used": True,
483
+ "budget_trimmed": fb_trim,
484
+ "error": "skipped_plugin_flow",
485
+ "elapsed_sec": 0.0,
486
+ }
487
+ print(json.dumps(out), flush=True)
488
+ return 0
489
+
464
490
  voice_relationship = args.voice_relationship or resolve_voice_relationship(args.project)
465
491
  # Length cap is X-specific; reddit/linkedin pass None (no tail trim).
466
492
  limit = TWEET_LIMIT if args.platform == "twitter" else None