@m13v/s4l 1.7.1-rc.8 → 1.7.1-rc.9

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/bin/cli.js CHANGED
@@ -823,6 +823,29 @@ function installMcp() {
823
823
  } catch (e) {
824
824
  console.warn(' WARNING: could not stamp MCP version:', e && e.message);
825
825
  }
826
+ // Auto-opt a fresh install into the staging channel when the version being
827
+ // installed is itself a pre-release (-rc.N) — e.g. `npx social-autoposter@
828
+ // X.Y.Z-rc.N init`. Without this, channel.json stays absent, which
829
+ // scripts/s4l_channel.py's fail-safe default reads as "stable", so the box
830
+ // would install this one rc and then silently stop tracking staging (never
831
+ // pick up the next rc). Mirrors the same one-time write in
832
+ // mcp/src/runtime.ts's provision() for the .mcpb (Desktop) install path.
833
+ // Only writes when no channel marker exists yet — never overrides an
834
+ // existing preference (e.g. a user who already opted back out).
835
+ try {
836
+ const pkgVersion = require('../package.json').version;
837
+ if (pkgVersion.includes('-rc.')) {
838
+ const stateDir = process.env.S4L_STATE_DIR || path.join(os.homedir(), '.social-autoposter-mcp');
839
+ const channelPath = path.join(stateDir, 'channel.json');
840
+ if (!fs.existsSync(channelPath)) {
841
+ fs.mkdirSync(stateDir, { recursive: true });
842
+ fs.writeFileSync(channelPath, JSON.stringify({ channel: 'staging' }, null, 2) + '\n');
843
+ console.log(' pre-release install detected -> opted into the staging channel');
844
+ }
845
+ }
846
+ } catch (e) {
847
+ console.warn(' WARNING: could not set staging channel:', e && e.message);
848
+ }
826
849
  console.log(' installing MCP runtime deps (npm install --omit=dev in mcp/)');
827
850
  const npmRes = spawnSync('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], {
828
851
  cwd: mcpDest,
@@ -676,6 +676,29 @@ async function provision(progress) {
676
676
  /* best effort; S4L_REPO_DIR + the run-*.sh fallback also resolve the repo */
677
677
  }
678
678
  setStep("repo", "done", `unpacked to ${resolvedRepo}`);
679
+ // Auto-opt a bare .mcpb FIRST install into the staging channel when the
680
+ // shipped build is itself a pre-release (-rc.N) — e.g. downloaded from
681
+ // the staging-latest alias link (scripts/release-mcpb.sh step 7b).
682
+ // Without this, channel.json stays absent, which releaseChannel()'s
683
+ // fail-safe default reads as "stable" (mcp/src/version.ts), so the box
684
+ // would install this one rc and then silently stop tracking staging
685
+ // (never pick up the next rc). Mirrors the same one-time write in
686
+ // bin/cli.js's installMcp() for the npx install path. Gated to this
687
+ // `else` branch (genuine bare-.mcpb unpack, not envClone) so a
688
+ // developer's own npm/git clone is never auto-enrolled. Only writes when
689
+ // no channel marker exists yet — never overrides an existing preference.
690
+ try {
691
+ const bv = bundledVersion();
692
+ const channelPath = path.join(STATE_DIR, "channel.json");
693
+ if (bv && bv.includes("-rc.") && !fs.existsSync(channelPath)) {
694
+ fs.mkdirSync(STATE_DIR, { recursive: true });
695
+ fs.writeFileSync(channelPath, JSON.stringify({ channel: "staging" }, null, 2) + "\n", "utf-8");
696
+ console.error(`[runtime] first install of a staging build (${bv}) — opted into the staging channel`);
697
+ }
698
+ }
699
+ catch {
700
+ /* best effort; worst case the box just tracks stable */
701
+ }
679
702
  }
680
703
  // --- Step 1: uv -----------------------------------------------------------
681
704
  setStep("uv", "running");
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.7.1-rc.8",
3
- "installedAt": "2026-07-07T23:28:11.193Z"
2
+ "version": "1.7.1-rc.9",
3
+ "installedAt": "2026-07-08T00:22:25.267Z"
4
4
  }
package/mcp/install.mjs CHANGED
@@ -14,8 +14,6 @@ import path from "node:path";
14
14
 
15
15
  const SERVER_KEY = "social-autoposter";
16
16
  const UNINSTALL = process.argv.includes("--uninstall");
17
- const CHANNEL_STAGING = process.argv.includes("--channel-staging");
18
- const CHANNEL_STABLE = process.argv.includes("--channel-stable");
19
17
 
20
18
  // ---- resolve the absolute paths we want pinned into the spawn env ----------
21
19
  const here = path.dirname(new URL(import.meta.url).pathname);
@@ -137,19 +135,6 @@ for (const t of targets) {
137
135
  }
138
136
  }
139
137
 
140
- // Set release channel if requested
141
- if (!UNINSTALL && (CHANNEL_STAGING || CHANNEL_STABLE)) {
142
- const stateDir = path.join(home, ".social-autoposter-mcp");
143
- const channel = CHANNEL_STAGING ? "staging" : "stable";
144
- try {
145
- fs.mkdirSync(stateDir, { recursive: true });
146
- writeJson(path.join(stateDir, "channel.json"), { channel });
147
- console.log(`✓ Release channel set to: ${channel}`);
148
- } catch (err) {
149
- console.error(`✗ Failed to set channel: ${err.message}`);
150
- }
151
- }
152
-
153
138
  console.log("");
154
139
  if (ok === targets.length) {
155
140
  if (UNINSTALL) {
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.7.1-rc.8",
5
+ "version": "1.7.1-rc.9",
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": {
@@ -1801,15 +1801,13 @@ class S4LMenuBar(rumps.App):
1801
1801
  return
1802
1802
  _activate_front()
1803
1803
  choice = _show_alert(
1804
- title="Tidy the S4L background tasks?",
1804
+ title="Tidy S4L background tasks?",
1805
1805
  message=(
1806
- "S4L can tidy its background tasks: merge the old draft + query "
1807
- "tasks into ONE universal worker (s4l-worker) and make sure "
1808
- "their once-a-minute runs stay in a dedicated folder instead of "
1809
- "cluttering your `claude --resume` history.\n\n"
1810
- "Claude Desktop will restart once to apply — its window will "
1811
- "close and reopen in a moment. Your X login, drafts, and config "
1812
- "all stay."
1806
+ "Move the background worker to a dedicated folder so its "
1807
+ "once-a-minute runs stop cluttering your `claude --resume` "
1808
+ "history.\n\n"
1809
+ "Claude Desktop will restart once to apply. Your X login, "
1810
+ "drafts, and config all stay."
1813
1811
  ),
1814
1812
  ok="Tidy & restart Claude", cancel="Later",
1815
1813
  )
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l-mcp",
3
- "version": "1.7.1-rc.8",
3
+ "version": "1.7.1-rc.9",
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",
@@ -149,6 +149,14 @@ function runDoctorSync(options = {}) {
149
149
  "deferred until X connection to avoid an unexpected keychain prompt"
150
150
  );
151
151
  }
152
+ // Once the durable cookie mirror is populated, the X session no longer
153
+ // depends on this keychain item (see twitter_cookie_mirror.py — it exists
154
+ // precisely so a locked/missing Safe Storage key isn't fatal). Skip the
155
+ // probe instead of re-triggering + re-failing it on every doctor run;
156
+ // same mirrorCount() gate keychain_autolock already uses below.
157
+ if (mirrorCount() > 0) {
158
+ return result("pass", "skipped — durable cookie mirror already covers this");
159
+ }
152
160
  const r = spawnSync(
153
161
  "security",
154
162
  [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l",
3
- "version": "1.7.1-rc.8",
3
+ "version": "1.7.1-rc.9",
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",
@@ -1,67 +1,50 @@
1
1
  #!/bin/bash
2
- # Get the permanent download URL for the latest S4L staging release (.mcpb).
2
+ # Print (or download) the permanent download link for the latest S4L staging
3
+ # release (.mcpb).
4
+ #
3
5
  # Usage:
4
- # bash scripts/get-latest-staging-mcpb.sh [--download] [--channel] [--path]
6
+ # bash scripts/get-latest-staging-mcpb.sh [--download] [--channel]
5
7
  #
6
8
  # Options:
7
- # --download Download the .mcpb to current directory (instead of just printing URL)
8
- # --channel Also set the local channel to staging after download
9
- # --path Print the download path instead of the URL (for scripts)
9
+ # --download Download the .mcpb to the current directory (instead of
10
+ # just printing the URL)
11
+ # --channel After downloading, opt this machine into the staging
12
+ # channel (delegates to scripts/s4l_channel.py, the single
13
+ # source of truth for the channel marker)
14
+ #
15
+ # The link below is a FIXED URL, not a live GitHub API lookup: every
16
+ # `bash scripts/release-mcpb.sh --staging` run mirrors its .mcpb onto a
17
+ # separate, non-version-tagged release ("staging-latest") so this exact URL
18
+ # always serves whatever's newest — see scripts/release-mcpb.sh step 7b. That
19
+ # means this script needs zero network calls to resolve anything; it only
20
+ # hits the network when actually downloading.
10
21
 
11
22
  set -euo pipefail
12
23
 
24
+ STAGING_MCPB_URL="https://github.com/m13v/s4l/releases/download/staging-latest/social-autoposter.mcpb"
25
+
13
26
  DO_DOWNLOAD=0
14
27
  DO_CHANNEL=0
15
- DO_PATH=0
16
28
 
17
29
  while [[ $# -gt 0 ]]; do
18
30
  case "$1" in
19
31
  --download) DO_DOWNLOAD=1; shift ;;
20
32
  --channel) DO_CHANNEL=1; shift ;;
21
- --path) DO_PATH=1; shift ;;
22
- *) echo "Unknown option: $1"; exit 1 ;;
33
+ *) echo "Unknown option: $1" >&2; exit 1 ;;
23
34
  esac
24
35
  done
25
36
 
26
- # Fetch the latest pre-release from GitHub API (first page, per_page=1 for speed)
27
- # Response is a JSON array; use python to extract the first object
28
- RELEASE_DATA=$(curl -fsS "https://api.github.com/repos/m13v/s4l/releases?per_page=1&pre_release=true" 2>/dev/null | python3 -c "import sys, json; data=json.load(sys.stdin); r=data[0] if data else {}; print(json.dumps(r))")
29
-
30
- # Extract version and download URL using python for reliable JSON parsing
31
- LATEST_TAG=$(echo "$RELEASE_DATA" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('tag_name', ''))" 2>/dev/null)
32
- MCPB_URL=$(echo "$RELEASE_DATA" | python3 -c "
33
- import sys, json
34
- data = json.load(sys.stdin)
35
- for asset in data.get('assets', []):
36
- if 'social-autoposter.mcpb' in asset.get('name', ''):
37
- print(asset.get('browser_download_url', ''))
38
- break
39
- " 2>/dev/null)
40
-
41
- if [ -z "$LATEST_TAG" ] || [ -z "$MCPB_URL" ]; then
42
- echo "Error: Could not find latest staging release" >&2
43
- exit 1
44
- fi
45
-
46
- if [ "$DO_PATH" -eq 1 ]; then
47
- # Return the tag only (for scripting)
48
- echo "$LATEST_TAG"
37
+ if [ "$DO_DOWNLOAD" -eq 0 ]; then
38
+ echo "$STAGING_MCPB_URL"
49
39
  exit 0
50
40
  fi
51
41
 
52
- if [ "$DO_DOWNLOAD" -eq 1 ]; then
53
- FILENAME="social-autoposter-${LATEST_TAG}.mcpb"
54
- echo "Downloading $FILENAME from $MCPB_URL" >&2
55
- curl -fL "$MCPB_URL" -o "$FILENAME"
56
- echo "$FILENAME"
42
+ FILENAME="social-autoposter-staging.mcpb"
43
+ echo "Downloading $FILENAME from $STAGING_MCPB_URL" >&2
44
+ curl -fL "$STAGING_MCPB_URL" -o "$FILENAME"
45
+ echo "$FILENAME"
57
46
 
58
- if [ "$DO_CHANNEL" -eq 1 ]; then
59
- STATE_DIR="${S4L_STATE_DIR:-$HOME/.social-autoposter-mcp}"
60
- mkdir -p "$STATE_DIR"
61
- echo '{"channel":"staging"}' > "$STATE_DIR/channel.json"
62
- echo "✓ Channel set to staging" >&2
63
- fi
64
- else
65
- # Just print the URL
66
- echo "$MCPB_URL"
47
+ if [ "$DO_CHANNEL" -eq 1 ]; then
48
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
49
+ python3 "$REPO_ROOT/scripts/s4l_channel.py" set staging >&2
67
50
  fi
@@ -45,6 +45,16 @@
45
45
  # the EXACT tested artifact ships to everyone. Nothing can drift between test and
46
46
  # ship because there is no repack.
47
47
  #
48
+ # FIXED STAGING DOWNLOAD LINK (2026-07-07, step 7b): stable has a permanent
49
+ # "latest" URL (releases/latest/download/social-autoposter.mcpb); staging did
50
+ # not, because GitHub's releases/latest hard-excludes prereleases with no
51
+ # override. Every --staging run now also mirrors the .mcpb onto a separate,
52
+ # non-version-tagged release ("staging-latest") so this URL never changes:
53
+ # https://github.com/m13v/s4l/releases/download/staging-latest/social-autoposter.mcpb
54
+ # The tag deliberately isn't semver-shaped, so version.ts/snapshot.py's real
55
+ # "newest rc" resolution (which requires a ^\d+\.\d+\.\d+ tag) ignores it —
56
+ # it is a convenience download link only, never a candidate update source.
57
+ #
48
58
  # Usage:
49
59
  # bash scripts/release-mcpb.sh # patch bump, npm + .mcpb + GitHub (STABLE)
50
60
  # bash scripts/release-mcpb.sh --bump minor
@@ -68,6 +78,12 @@ MCP_DIR="$REPO_ROOT/mcp"
68
78
  BUNDLE="$MCP_DIR/social-autoposter.mcpb"
69
79
  GH_REPO="m13v/s4l"
70
80
  SIZE_CAP_MB=180
81
+ # Floating alias release for a fixed "always the newest staging build" download
82
+ # link, mirroring what releases/latest/download/<asset> already gives stable.
83
+ # GitHub's own releases/latest excludes prereleases with no override, so there is
84
+ # no built-in equivalent for the rc channel — see step 7b below for why a
85
+ # non-semver tag here is safe (never confuses the real update-detection code).
86
+ STAGING_ALIAS_TAG="staging-latest"
71
87
 
72
88
  TAG_OVERRIDE=""
73
89
  VERSION_OVERRIDE=""
@@ -496,6 +512,38 @@ say "Released $TAG"
496
512
  echo " asset: social-autoposter.mcpb (${MB}MB)"
497
513
  [[ -n "$URL" ]] && echo " $URL"
498
514
 
515
+ # ---- 7b. Fixed "always latest staging" download link ------------------------
516
+ # Mirrors the asset just uploaded above onto a SEPARATE release tagged
517
+ # "$STAGING_ALIAS_TAG" (not a version), so the download URL never changes:
518
+ # https://github.com/$GH_REPO/releases/download/$STAGING_ALIAS_TAG/social-autoposter.mcpb
519
+ # Safe by construction: latestFromGithubListStaging() (mcp/src/version.ts) and
520
+ # _latest_from_github_list_staging() (scripts/snapshot.py) both require
521
+ # tag_name to match ^\d+\.\d+\.\d+ before treating a release as a candidate
522
+ # "newest" build; a tag literally named "staging-latest" fails that check and
523
+ # is silently skipped by both, so this alias can never get mistaken for a real
524
+ # rc and can never confuse which build a box installs. It exists ONLY to give
525
+ # humans (and scripts) one fixed bookmark — actual boxes still resolve "newest"
526
+ # from the real per-release rc tags via the releases LIST endpoint, unchanged.
527
+ if [[ "$DO_STAGING" == "1" ]]; then
528
+ say "Mirroring asset onto the fixed staging-latest alias"
529
+ ALIAS_NOTES="Always mirrors the newest staging pre-release's .mcpb asset (currently $TAG).
530
+
531
+ Fixed download link (never changes): https://github.com/$GH_REPO/releases/download/$STAGING_ALIAS_TAG/social-autoposter.mcpb
532
+
533
+ Not a real release — do not install from the tag page; use the asset link above. Actual per-build releases are tagged by version (e.g. $TAG)."
534
+ if gh release view "$STAGING_ALIAS_TAG" -R "$GH_REPO" >/dev/null 2>&1; then
535
+ gh release upload "$STAGING_ALIAS_TAG" "$BUNDLE" -R "$GH_REPO" --clobber
536
+ gh release edit "$STAGING_ALIAS_TAG" -R "$GH_REPO" --notes "$ALIAS_NOTES" >/dev/null
537
+ else
538
+ gh release create "$STAGING_ALIAS_TAG" "$BUNDLE" \
539
+ -R "$GH_REPO" \
540
+ --title "social-autoposter (staging — always latest)" \
541
+ --notes "$ALIAS_NOTES" \
542
+ --prerelease
543
+ fi
544
+ echo " https://github.com/$GH_REPO/releases/download/$STAGING_ALIAS_TAG/social-autoposter.mcpb"
545
+ fi
546
+
499
547
  # ---- 8. Verify the update banner will fire ---------------------------------
500
548
  # The menu-bar "⬆ Update available" banner (mcp/src/version.ts::versionStatus)
501
549
  # resolves "latest" from GitHub releases/latest, which is what .mcpb boxes (no
@@ -1,29 +0,0 @@
1
- #!/bin/bash
2
- # Set the S4L release channel (stable or staging).
3
- # Usage:
4
- # bash scripts/set-channel.sh stable # track stable releases only (default)
5
- # bash scripts/set-channel.sh staging # track staging releases (pre-releases)
6
-
7
- set -euo pipefail
8
-
9
- if [ $# -lt 1 ]; then
10
- echo "Usage: bash scripts/set-channel.sh <stable|staging>" >&2
11
- exit 1
12
- fi
13
-
14
- CHANNEL="$1"
15
- if [ "$CHANNEL" != "stable" ] && [ "$CHANNEL" != "staging" ]; then
16
- echo "Error: channel must be 'stable' or 'staging'" >&2
17
- exit 1
18
- fi
19
-
20
- STATE_DIR="${S4L_STATE_DIR:-$HOME/.social-autoposter-mcp}"
21
- mkdir -p "$STATE_DIR"
22
- echo "{\"channel\":\"$CHANNEL\"}" > "$STATE_DIR/channel.json"
23
-
24
- echo "✓ Release channel set to: $CHANNEL"
25
- if [ "$CHANNEL" = "staging" ]; then
26
- echo " Will now track pre-releases (rc.N builds). Next update check pulls the latest staging release."
27
- else
28
- echo " Will now track stable releases only. Next update check pulls the latest stable release."
29
- fi