@m13v/s4l 1.7.4-rc.9 → 1.7.5-rc.1
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/README.md +134 -101
- package/bin/cli.js +23 -0
- package/mcp/dist/index.js +71 -5
- package/mcp/dist/panel.html +31 -16
- package/mcp/dist/product-link.html +1 -1
- package/mcp/dist/runtime.js +110 -0
- package/mcp/dist/version.js +163 -54
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +5 -1
- package/mcp/menubar/s4l_card.py +55 -19
- package/mcp/menubar/s4l_menubar.py +237 -56
- package/mcp/menubar/s4l_state.py +113 -1
- package/mcp/package.json +1 -1
- package/package.json +3 -1
- package/scripts/active_experiments.py +30 -6
- package/scripts/autopilot_stall_watch.py +185 -63
- package/scripts/cdp_ready_check.py +94 -0
- package/scripts/engage_twitter_helper.py +83 -17
- package/scripts/engagement_styles.py +221 -26
- package/scripts/enrich_reply_parents.py +68 -22
- package/scripts/feedback_digest.py +24 -1
- package/scripts/generate_daily_human_style.py +31 -7
- package/scripts/invent_styles.py +103 -26
- package/scripts/linkedin_cadence.py +234 -0
- package/scripts/linkedin_killswitch.py +43 -0
- package/scripts/memory_snapshot.py +57 -0
- package/scripts/patches/browser-harness-loopback-cdp-proxy.patch +118 -0
- package/scripts/recent_self_posts.py +6 -0
- package/scripts/reddit_browser.py +57 -3
- package/scripts/s4l_box_update.sh +20 -1
- package/scripts/s4l_mode.py +3 -4
- package/scripts/s4l_posting_mode.py +59 -0
- package/scripts/schedule_state.py +23 -0
- package/scripts/scheduled_task_selfheal.py +213 -16
- package/scripts/setup_twitter_auth.py +6 -1
- package/scripts/snapshot.py +179 -61
- package/scripts/stall_guard.py +70 -0
- package/scripts/test_scheduled_task_selfheal.py +121 -0
- package/scripts/twitter_browser.py +146 -8
- package/scripts/twitter_post_plan.py +10 -1
- package/scripts/twitter_scan.py +26 -0
- package/scripts/watchdog_hung_runs.py +117 -3
- package/skill/amplitude-24h-signups.sh +1 -1
- package/skill/audit-dm-staleness.sh +1 -1
- package/skill/audit-linkedin.sh +1 -1
- package/skill/audit-reddit-resurrect.sh +1 -1
- package/skill/audit.sh +5 -5
- package/skill/check-web-chats.sh +1 -1
- package/skill/dm-outreach-linkedin.sh +4 -4
- package/skill/dm-outreach-reddit.sh +1 -1
- package/skill/dm-outreach-twitter.sh +1 -1
- package/skill/engage-dm-replies.sh +16 -3
- package/skill/engage-linkedin.sh +2 -2
- package/skill/engage-moltbook.sh +1 -1
- package/skill/engage-twitter.sh +4 -4
- package/skill/github-engage.sh +1 -1
- package/skill/lib/browser-launch.sh +54 -0
- package/skill/lib/linkedin-backend.sh +22 -15
- package/skill/lib/reddit-backend.sh +17 -11
- package/skill/lib/twitter-backend.sh +137 -16
- package/skill/link-edit-github.sh +1 -1
- package/skill/link-edit-moltbook.sh +1 -1
- package/skill/link-edit-reddit.sh +1 -1
- package/skill/linkedin-cadence.sh +20 -0
- package/skill/linkedin-presence.sh +1 -1
- package/skill/lock.sh +30 -6
- package/skill/precompute-stats.sh +1 -1
- package/skill/refresh-instagram-tokens.sh +1 -1
- package/skill/refresh-twitter-following.sh +1 -1
- package/skill/run-generate-daily-style.sh +1 -1
- package/skill/run-instagram-daily.sh +1 -1
- package/skill/run-instagram-render.sh +1 -1
- package/skill/run-linkedin-launchd.sh +1 -1
- package/skill/run-linkedin-unipile.sh +1 -1
- package/skill/run-linkedin.sh +1 -1
- package/skill/run-reddit-search.sh +2 -2
- package/skill/run-reddit-threads.sh +1 -1
- package/skill/run-twitter-cycle.sh +162 -54
- package/skill/scan-instagram-replies.sh +1 -1
- package/skill/sentry-digest.sh +1 -1
- package/skill/stats-instagram.sh +1 -1
- package/skill/stats-linkedin.sh +2 -2
- package/skill/stats.sh +1 -1
- package/skill/sweep-link-clicks.sh +1 -1
- package/scripts/_lock_preempt_test.py +0 -60
- package/scripts/saps_activity.py +0 -18
- package/scripts/saps_mode.py +0 -18
package/README.md
CHANGED
|
@@ -1,143 +1,176 @@
|
|
|
1
|
-
#
|
|
1
|
+
# S4L
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
S4L is a desktop plugin for running a Reddit and X reply-drafting workflow from
|
|
4
|
+
your own machine.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
It watches the conversations your buyers already read, finds threads with real
|
|
7
|
+
momentum and fit, drafts contribution-first replies in your voice, and gives you
|
|
8
|
+
a review queue before anything goes live. The product is positioned as a
|
|
9
|
+
self-serve desktop plugin: subscribe, install, connect your accounts, review the
|
|
10
|
+
drafts, and track the results.
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
- Website: https://s4l.ai
|
|
13
|
+
- Pricing: https://s4l.ai/pricing
|
|
14
|
+
- Source: https://github.com/m13v/social-autoposter
|
|
8
15
|
|
|
9
|
-
##
|
|
16
|
+
## What S4L does
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
Most social tools start after you already know what to post. S4L starts earlier:
|
|
19
|
+
it looks for conversations worth entering.
|
|
12
20
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
-
|
|
21
|
+
- Finds high-traffic Reddit and X threads with enough context to join well.
|
|
22
|
+
- Ranks opportunities by momentum, intent, community norms, and product fit.
|
|
23
|
+
- Drafts replies that answer the thread first and mention your product only when
|
|
24
|
+
it belongs.
|
|
25
|
+
- Keeps project-specific voice, claim language, competitors, sensitive topics,
|
|
26
|
+
and hard lines in the drafting context.
|
|
27
|
+
- Presents drafts in a review workflow so you can approve, reject, or edit before
|
|
28
|
+
posting.
|
|
29
|
+
- Tracks views, upvotes, clicks, deletions, and misses so each run learns from
|
|
30
|
+
the previous one.
|
|
18
31
|
|
|
19
|
-
|
|
32
|
+
Nothing is posted automatically by default. Posting autopilot stays off until you
|
|
33
|
+
explicitly turn it on.
|
|
20
34
|
|
|
21
|
-
|
|
22
|
-
- `RESEND_API_KEY` and `NOTIFICATION_EMAIL` in `.env` for DM-escalation emails
|
|
35
|
+
## Who it is for
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
S4L is for founders and small teams who want an AI-assisted social workflow
|
|
38
|
+
without handing their accounts to an agency or living inside mention alerts all
|
|
39
|
+
day.
|
|
25
40
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```
|
|
41
|
+
You bring the accounts, product context, voice, and judgment. The plugin handles
|
|
42
|
+
thread discovery, draft generation, review cards, scheduling, and result memory.
|
|
29
43
|
|
|
30
|
-
|
|
44
|
+
## How users install it
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
2. Creates `config.json` from `config.example.json` and writes a blank `.env` template (fill in your S4L API key and optional `MOLTBOOK_API_KEY`)
|
|
34
|
-
3. Installs the Python helper deps via `pip3` if missing
|
|
35
|
-
4. Generates launchd plists in `~/social-autoposter/launchd/` with the user's actual `HOME` and `PATH`
|
|
36
|
-
5. Installs the Playwright MCP configs to `~/.claude/browser-agent-configs/` (twitter, reddit, linkedin) with `__HOME__` and `__NODE_BIN__` placeholders substituted. Existing files are left alone, so any window-position tweaks survive `npx social-autoposter update`.
|
|
37
|
-
6. Creates empty persistent browser profile dirs at `~/.claude/browser-profiles/{twitter,reddit,linkedin}`
|
|
38
|
-
7. Symlinks `~/.claude/skills/social-autoposter` and `~/.claude/skills/social-autoposter-setup` to the install dir
|
|
46
|
+
The production path is the desktop plugin download from S4L.
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
1. Subscribe at https://s4l.ai/pricing.
|
|
49
|
+
2. Download the plugin link emailed after checkout.
|
|
50
|
+
3. Install the plugin in Claude Desktop.
|
|
51
|
+
4. Start a new Claude chat and send:
|
|
41
52
|
|
|
42
|
-
```
|
|
43
|
-
|
|
53
|
+
```text
|
|
54
|
+
Set me up on S4L plugin end to end
|
|
44
55
|
```
|
|
45
56
|
|
|
46
|
-
|
|
57
|
+
The setup flow repairs the local runtime, connects your X browser session,
|
|
58
|
+
discovers product and voice context, seeds search topics, schedules draft
|
|
59
|
+
generation, and verifies that draft cards appear without posting.
|
|
47
60
|
|
|
48
|
-
|
|
49
|
-
setup skill treats that as a terminal goal:
|
|
61
|
+
## What is in this repo
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
2. Auto-detect the best browser profile and connect X/Twitter. macOS may require
|
|
53
|
-
a Safe Storage approval; a logged-out account may require one manual sign-in.
|
|
54
|
-
3. Scan the X profile, discover and research the user's product, and infer a
|
|
55
|
-
conservative project, ICP, voice, and search topics without an interview.
|
|
56
|
-
4. Save the project and seed its topics into the backend.
|
|
57
|
-
5. Run a draft-only cycle to verify the pipeline without posting.
|
|
63
|
+
This repository contains the open-source runtime behind the S4L plugin:
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
```text
|
|
66
|
+
social-autoposter/
|
|
67
|
+
|-- mcp/ Desktop plugin / MCP server, panel UI, release bundle
|
|
68
|
+
|-- scripts/ Discovery, drafting, stats, telemetry, and queue helpers
|
|
69
|
+
|-- skill/ Shell entrypoints used by scheduled jobs
|
|
70
|
+
|-- setup/ End-to-end setup skill used by Claude
|
|
71
|
+
|-- browser-agent-configs/ Browser automation profile templates
|
|
72
|
+
|-- launchd/ macOS LaunchAgent templates
|
|
73
|
+
|-- mcp-servers/ Local MCP helpers used by the runtime
|
|
74
|
+
|-- config.example.json Example project/account configuration
|
|
75
|
+
`-- SKILL.md Legacy social-autoposter agent playbook
|
|
76
|
+
```
|
|
61
77
|
|
|
62
|
-
|
|
78
|
+
The `mcp/` package is the plugin users interact with. The rest of the repo is the
|
|
79
|
+
pipeline it bundles, installs, and drives.
|
|
63
80
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
├──▶ scripts/top_performers.py (feedback report from past stats)
|
|
74
|
-
└──▶ S4L HTTP API (AUTOPOSTER_API_BASE in .env)
|
|
81
|
+
## Architecture
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
Claude Desktop plugin
|
|
85
|
+
-> S4L MCP server
|
|
86
|
+
-> local runtime + menu bar review UI
|
|
87
|
+
-> scheduled queue worker
|
|
88
|
+
-> browser profiles you control
|
|
89
|
+
-> S4L API for install-scoped queues, stats, and configuration
|
|
75
90
|
```
|
|
76
91
|
|
|
77
|
-
|
|
92
|
+
Important properties:
|
|
78
93
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
- Runs locally on macOS.
|
|
95
|
+
- Uses the user's own logged-in browser profiles.
|
|
96
|
+
- Stores install state locally and scopes API calls by install identity.
|
|
97
|
+
- Keeps secrets such as `.env`, `config.json`, browser profiles, logs, and local
|
|
98
|
+
databases out of Git.
|
|
99
|
+
- Uses approval-first drafting as the default operating mode.
|
|
85
100
|
|
|
86
|
-
|
|
101
|
+
## Develop from source
|
|
87
102
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
| `com.m13v.social-stats` (`stats.sh`) | every 21600 s (6 h) |
|
|
91
|
-
| `com.m13v.social-engage` (`engage.sh`) | every 21600 s (6 h) |
|
|
103
|
+
For normal users, use the plugin download from S4L. These steps are for working
|
|
104
|
+
on the repo itself.
|
|
92
105
|
|
|
93
|
-
|
|
106
|
+
Prerequisites:
|
|
107
|
+
|
|
108
|
+
- macOS
|
|
109
|
+
- Node.js 16+
|
|
110
|
+
- Python 3.9+
|
|
111
|
+
- Claude Desktop or Claude Code for MCP testing
|
|
112
|
+
|
|
113
|
+
Install dependencies:
|
|
94
114
|
|
|
95
115
|
```bash
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
npm install
|
|
117
|
+
cd mcp
|
|
118
|
+
npm install
|
|
98
119
|
```
|
|
99
120
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
| Command | What it does |
|
|
103
|
-
|---------|-------------|
|
|
104
|
-
| `/social-autoposter` | Comment run: find threads, draft, post, log (cron-safe) |
|
|
105
|
-
| `/social-autoposter post` | Create an original post or thread (manual only) |
|
|
106
|
-
| `/social-autoposter stats` | Update engagement stats via API |
|
|
107
|
-
| `/social-autoposter engage` | Scan and reply to responses on our posts |
|
|
108
|
-
| `/social-autoposter audit` | Full browser audit of all posts |
|
|
121
|
+
Build the plugin server and panel:
|
|
109
122
|
|
|
110
|
-
|
|
123
|
+
```bash
|
|
124
|
+
cd mcp
|
|
125
|
+
npm run build
|
|
126
|
+
```
|
|
111
127
|
|
|
112
|
-
|
|
128
|
+
Register this checkout with Claude Desktop and Claude Code for local testing:
|
|
113
129
|
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
├── bin/cli.js installer + dashboard launcher
|
|
118
|
-
├── browser-agent-configs/ Playwright MCP templates (twitter/reddit/linkedin)
|
|
119
|
-
├── config.example.json config template
|
|
120
|
-
├── setup/SKILL.md autonomous end-to-end setup skill (locked)
|
|
121
|
-
├── scripts/ Python and JS helpers (no browser, no LLM)
|
|
122
|
-
├── skill/ shell wrappers invoked by launchd
|
|
123
|
-
└── launchd/ generated macOS LaunchAgent plists
|
|
130
|
+
```bash
|
|
131
|
+
cd mcp
|
|
132
|
+
node install.mjs
|
|
124
133
|
```
|
|
125
134
|
|
|
126
|
-
|
|
135
|
+
Then fully quit and reopen Claude so MCP servers reload.
|
|
127
136
|
|
|
128
|
-
|
|
137
|
+
Run the test suite:
|
|
129
138
|
|
|
130
|
-
|
|
139
|
+
```bash
|
|
140
|
+
npm test
|
|
141
|
+
```
|
|
131
142
|
|
|
132
|
-
|
|
143
|
+
Build a local `.mcpb` artifact without publishing:
|
|
133
144
|
|
|
134
145
|
```bash
|
|
135
|
-
|
|
136
|
-
for plist in ~/Library/LaunchAgents/com.m13v.social-*.plist; do launchctl unload "$plist"; done
|
|
137
|
-
|
|
138
|
-
# Resume: reload all jobs
|
|
139
|
-
for plist in ~/social-autoposter/launchd/com.m13v.social-*.plist; do
|
|
140
|
-
ln -sf "$plist" ~/Library/LaunchAgents/
|
|
141
|
-
launchctl load ~/Library/LaunchAgents/$(basename "$plist")
|
|
142
|
-
done
|
|
146
|
+
bash scripts/release-mcpb.sh --no-bump --no-npm --no-release
|
|
143
147
|
```
|
|
148
|
+
|
|
149
|
+
That command packs the current pipeline into `mcp/dist/pipeline.tgz`, builds the
|
|
150
|
+
plugin, creates `mcp/social-autoposter.mcpb`, and runs the release checks without
|
|
151
|
+
touching npm or GitHub releases.
|
|
152
|
+
|
|
153
|
+
## Runtime commands
|
|
154
|
+
|
|
155
|
+
The plugin exposes MCP tools for the user-facing workflow:
|
|
156
|
+
|
|
157
|
+
- `project_config` configures projects, products, voice, topics, and X auth.
|
|
158
|
+
- `engagement_mode` chooses personal-brand and product-promotion lanes.
|
|
159
|
+
- `dashboard` opens the review dashboard.
|
|
160
|
+
- `post_drafts` posts only the drafts the user selected.
|
|
161
|
+
- `get_stats` reads X/Twitter stats.
|
|
162
|
+
- `pause_s4l` pauses or resumes scheduled S4L jobs.
|
|
163
|
+
- `runtime` installs, updates, and diagnoses the local runtime.
|
|
164
|
+
- `report_diagnosis` sends a support report to the S4L team.
|
|
165
|
+
|
|
166
|
+
The legacy `/social-autoposter` skill and npm package remain in the repo because
|
|
167
|
+
the plugin bundles and reuses the same pipeline scripts.
|
|
168
|
+
|
|
169
|
+
## Public-repo hygiene
|
|
170
|
+
|
|
171
|
+
This repo is public. Do not commit local customer data, browser state, generated
|
|
172
|
+
media, private automation experiments, `.mcpb` bundles, `.env` files, databases,
|
|
173
|
+
or logs. The root `.gitignore` intentionally keeps those out of source control.
|
|
174
|
+
|
|
175
|
+
If a workflow needs private scratch space, keep it outside the repo or under an
|
|
176
|
+
ignored directory.
|
package/bin/cli.js
CHANGED
|
@@ -427,6 +427,29 @@ function installBrowserHarness() {
|
|
|
427
427
|
if (reset.status !== 0) {
|
|
428
428
|
console.warn(' WARNING: could not reset browser-harness clone to pinned commit; using existing checkout.');
|
|
429
429
|
}
|
|
430
|
+
// Vendored fix on top of the pin (keep in sync with runtime.ts): loopback
|
|
431
|
+
// CDP requests must never route through a proxy — macOS system proxy
|
|
432
|
+
// settings leak into urllib's default opener and a box-wide forwarder
|
|
433
|
+
// 403s every 127.0.0.1 probe (2026-07-13). Applied at install time since
|
|
434
|
+
// we don't control the upstream repo. If it stops applying, upstream has
|
|
435
|
+
// either merged the fix (detected; fine) or refactored (warn); never fail
|
|
436
|
+
// the install over it.
|
|
437
|
+
const harnessPatch = path.join(PKG_ROOT, 'scripts', 'patches', 'browser-harness-loopback-cdp-proxy.patch');
|
|
438
|
+
if (fs.existsSync(harnessPatch)) {
|
|
439
|
+
const chk = spawnSync('git', ['-C', harnessDir, 'apply', '--check', harnessPatch], { stdio: 'pipe' });
|
|
440
|
+
if (chk.status === 0) {
|
|
441
|
+
spawnSync('git', ['-C', harnessDir, 'apply', harnessPatch], { stdio: 'inherit' });
|
|
442
|
+
console.log(' applied loopback-proxy patch to browser-harness');
|
|
443
|
+
} else {
|
|
444
|
+
let fixedUpstream = false;
|
|
445
|
+
try {
|
|
446
|
+
fixedUpstream = fs.readFileSync(path.join(harnessDir, 'src', 'browser_harness', 'admin.py'), 'utf-8').includes('cdp_urlopen');
|
|
447
|
+
} catch { /* file moved — fall through to the warning */ }
|
|
448
|
+
if (!fixedUpstream) {
|
|
449
|
+
console.warn(' WARNING: browser-harness loopback-proxy patch no longer applies and the fix is absent upstream; CDP probes may 403 behind a system proxy.');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
430
453
|
};
|
|
431
454
|
if (!fs.existsSync(harnessDir)) {
|
|
432
455
|
fs.mkdirSync(path.dirname(harnessDir), { recursive: true });
|
package/mcp/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import fs from "node:fs";
|
|
|
22
22
|
import { repoDir, runPython, run, readPlan, writePlan, planPath, } from "./repo.js";
|
|
23
23
|
import { applySetup, resolveProject, hasReadyProject, personaReady, listManagedProjectStatus, listProjectSettings, ensureShortLinksDefault, ensurePersonaProject, findPersonaProject, REQUIRED_FIELDS, RECOMMENDED_FIELDS, configPath, normalizeStringList, } from "./setup.js";
|
|
24
24
|
import { xStatus, xConnect, xDetectSources, xScanProfile, summarizeXAuth } from "./twitterAuth.js";
|
|
25
|
-
import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, resolveChrome, ensureMenubar, menubarRunning, clearMenubarStop, ensurePipelineCurrent, ensureRuntimeProvisioned, retryProvisionIfStalled, } from "./runtime.js";
|
|
25
|
+
import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, resolveChrome, ensureMenubar, menubarRunning, clearMenubarStop, ensurePipelineCurrent, ensureRuntimeProvisioned, ensureHarnessPatched, retryProvisionIfStalled, } from "./runtime.js";
|
|
26
26
|
import { blockOnboardingMilestone, completeOnboardingMilestone, ensureDoctorPhase, onboardingLedger, onboardingSnapshot, recordOnboardingAttempt, runDoctorPhase, } from "./onboarding.js";
|
|
27
27
|
import { VERSION, versionStatus, latestPublishedVersion } from "./version.js";
|
|
28
28
|
import { initSentry, sendHeartbeat, sendStateSnapshot, captureError, captureMessage, flushSentry, startLogStreaming, flushLogs, logLine, checkVersionChange } from "./telemetry.js";
|
|
@@ -163,6 +163,9 @@ function plistXml(opts) {
|
|
|
163
163
|
const schedule = opts.keepAlive
|
|
164
164
|
? `\t<key>KeepAlive</key>\n\t<true/>`
|
|
165
165
|
: `\t<key>StartInterval</key>\n\t<integer>${opts.intervalSecs}</integer>`;
|
|
166
|
+
const abandon = opts.abandonProcessGroup
|
|
167
|
+
? `\n\t<key>AbandonProcessGroup</key>\n\t<true/>`
|
|
168
|
+
: "";
|
|
166
169
|
// Background (cron/autopilot) runs get the same Chrome the interactive cycle
|
|
167
170
|
// uses, so a no-sudo ~/Applications install (which the shell's own resolver
|
|
168
171
|
// doesn't scan) is still found off-screen. Omitted when Chrome resolves via
|
|
@@ -188,7 +191,7 @@ function plistXml(opts) {
|
|
|
188
191
|
\t<array>
|
|
189
192
|
${args}
|
|
190
193
|
\t</array>
|
|
191
|
-
${schedule}
|
|
194
|
+
${schedule}${abandon}
|
|
192
195
|
\t<key>StandardOutPath</key>
|
|
193
196
|
\t<string>${opts.stdoutLog}</string>
|
|
194
197
|
\t<key>StandardErrorPath</key>
|
|
@@ -2673,6 +2676,53 @@ tool("restart_menubar", {
|
|
|
2673
2676
|
menubar_running: running,
|
|
2674
2677
|
});
|
|
2675
2678
|
});
|
|
2679
|
+
// ---- posting_volume: per-install posting-volume mode (virality bar) --------
|
|
2680
|
+
// Server-side throttle (2026-07-13): installations.posting_mode maps to a
|
|
2681
|
+
// virality-bar percentile on the API (high~0.90, medium~0.97, low~0.995) and
|
|
2682
|
+
// OVERRIDES the cycle driver's hardcoded percentile, so a change applies on
|
|
2683
|
+
// the next cycle with no client update. 'get' also returns per-mode estimated
|
|
2684
|
+
// posts/day replayed from THIS install's trailing-7d candidate pool.
|
|
2685
|
+
tool("posting_volume", {
|
|
2686
|
+
title: "Read or set posting volume (high / medium / low)",
|
|
2687
|
+
description: "Read or set this install's posting-volume mode, the quality bar that decides how many drafts " +
|
|
2688
|
+
"per day the twitter cycle produces. Three modes: high (~100+ posts/day), medium (~30/day), " +
|
|
2689
|
+
"low (~5/day, only the very best candidates); 'default' clears the override so the cycle's " +
|
|
2690
|
+
"built-in setting applies. action:'get' returns the current mode plus per-mode estimated " +
|
|
2691
|
+
"posts/day computed from this install's own recent candidate pool (show those numbers when " +
|
|
2692
|
+
"the user is choosing). Use when the user asks to post more, post less, slow down, raise the " +
|
|
2693
|
+
"quality bar, or change posting volume. Takes effect on the next cycle; in draft-review mode " +
|
|
2694
|
+
"it equally paces how many review cards appear.",
|
|
2695
|
+
inputSchema: {
|
|
2696
|
+
action: z.enum(["get", "set"]).default("get").describe("get = read mode + rates; set = change it"),
|
|
2697
|
+
mode: z
|
|
2698
|
+
.enum(["high", "medium", "low", "default"])
|
|
2699
|
+
.optional()
|
|
2700
|
+
.describe("Required for action:'set'. 'default' clears the override."),
|
|
2701
|
+
},
|
|
2702
|
+
}, async (args) => {
|
|
2703
|
+
const action = args.action || "get";
|
|
2704
|
+
if (action === "set") {
|
|
2705
|
+
if (!args.mode) {
|
|
2706
|
+
return jsonContent({ error: "mode is required for action:'set' (high|medium|low|default)" });
|
|
2707
|
+
}
|
|
2708
|
+
const r = await runPython("scripts/s4l_posting_mode.py", ["set", String(args.mode)], {
|
|
2709
|
+
timeoutMs: 30_000,
|
|
2710
|
+
});
|
|
2711
|
+
try {
|
|
2712
|
+
return jsonContent(JSON.parse((r.stdout || "").trim()));
|
|
2713
|
+
}
|
|
2714
|
+
catch {
|
|
2715
|
+
return jsonContent({ error: `posting-mode set failed: ${(r.stderr || r.stdout || "").slice(0, 300)}` });
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
const r = await runPython("scripts/s4l_posting_mode.py", ["get"], { timeoutMs: 30_000 });
|
|
2719
|
+
try {
|
|
2720
|
+
return jsonContent(JSON.parse((r.stdout || "").trim()));
|
|
2721
|
+
}
|
|
2722
|
+
catch {
|
|
2723
|
+
return jsonContent({ error: `posting-mode get failed: ${(r.stderr || r.stdout || "").slice(0, 300)}` });
|
|
2724
|
+
}
|
|
2725
|
+
});
|
|
2676
2726
|
// ---- pause_s4l: temporarily stop drafting/posting, reversibly --------------
|
|
2677
2727
|
// The lighter alternative to Quit: unloads only the launchd jobs that scan,
|
|
2678
2728
|
// draft, and post (plus their support daemons), leaving Claude Desktop, the
|
|
@@ -2985,9 +3035,11 @@ function queueDir() {
|
|
|
2985
3035
|
// routine is draining the queue — the worker would claim within a minute if it
|
|
2986
3036
|
// were firing. This is the liveness signal that survives a Claude account switch
|
|
2987
3037
|
// (which orphans the routines while their global SKILL.md files stay put, so the
|
|
2988
|
-
// SKILL.md-presence check in autopilotLoaded() reads a FALSE green). Mirrors
|
|
2989
|
-
//
|
|
2990
|
-
|
|
3038
|
+
// SKILL.md-presence check in autopilotLoaded() reads a FALSE green). Mirrors
|
|
3039
|
+
// HEALTHY_DRAIN_MAX_SECONDS in scripts/schedule_state.py, the single source of
|
|
3040
|
+
// truth every Python stall detector derives from; this TS constant can't import
|
|
3041
|
+
// it, so keep it in sync by hand when retuning there.
|
|
3042
|
+
const AUTOPILOT_STALL_MS = 1_200_000;
|
|
2991
3043
|
// True when no scheduled-task routine is draining the draft queue. Two signals,
|
|
2992
3044
|
// OR'd (keep in lockstep with mcp/menubar/s4l_menubar.py::_autopilot_stalled and
|
|
2993
3045
|
// scripts/autopilot_stall_watch.py):
|
|
@@ -3500,6 +3552,9 @@ async function ensureQueueKickerInstalled() {
|
|
|
3500
3552
|
stdoutLog: path.join(logDir, "launchd-twitter-cycle-stdout.log"),
|
|
3501
3553
|
stderrLog: path.join(logDir, "launchd-twitter-cycle-stderr.log"),
|
|
3502
3554
|
extraEnv: kickerEnv(),
|
|
3555
|
+
// Don't let launchd reap the harness Chrome the cycle launches when the
|
|
3556
|
+
// kicker shell exits (2026-07-12 foreground-steal loop).
|
|
3557
|
+
abandonProcessGroup: true,
|
|
3503
3558
|
});
|
|
3504
3559
|
// Content-aware install: an existing box has the OLD kicker plist pointing at
|
|
3505
3560
|
// run-twitter-cycle.sh (no merge step). ensurePlist won't overwrite, so detect
|
|
@@ -5101,6 +5156,17 @@ async function main() {
|
|
|
5101
5156
|
if (ensureRuntimeProvisioned()) {
|
|
5102
5157
|
console.error("[social-autoposter-mcp] owned runtime not ready; provisioning on boot");
|
|
5103
5158
|
}
|
|
5159
|
+
// Vendored harness patch for ALREADY-ready runtimes: provision() is skipped
|
|
5160
|
+
// above when the runtime is ready, so a patch shipped in an rc would never
|
|
5161
|
+
// reach an existing install's harness checkout without this. Idempotent
|
|
5162
|
+
// (marker check) and best-effort; never blocks boot.
|
|
5163
|
+
void ensureHarnessPatched()
|
|
5164
|
+
.then((r) => {
|
|
5165
|
+
if (!(r.ok && r.detail === "already patched")) {
|
|
5166
|
+
console.error(`[social-autoposter-mcp] harness patch: ${r.ok ? "ok" : "skip"} (${r.detail})`);
|
|
5167
|
+
}
|
|
5168
|
+
})
|
|
5169
|
+
.catch(() => { });
|
|
5104
5170
|
// Queue-backed drafting (2026-06-23): keep the two worker-task prompts current,
|
|
5105
5171
|
// pre-approve their tools EAGERLY (before onboarding even creates the tasks, so
|
|
5106
5172
|
// the first unattended fire can't stall), and (re)install the launchd kicker
|