@m13v/s4l 1.7.4-rc.9 → 1.7.4
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 +24 -5
- 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 +1 -1
- package/mcp/menubar/s4l_card.py +55 -19
- package/mcp/menubar/s4l_menubar.py +179 -56
- package/mcp/menubar/s4l_state.py +41 -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/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>
|
|
@@ -2985,9 +2988,11 @@ function queueDir() {
|
|
|
2985
2988
|
// routine is draining the queue — the worker would claim within a minute if it
|
|
2986
2989
|
// were firing. This is the liveness signal that survives a Claude account switch
|
|
2987
2990
|
// (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
|
-
|
|
2991
|
+
// SKILL.md-presence check in autopilotLoaded() reads a FALSE green). Mirrors
|
|
2992
|
+
// HEALTHY_DRAIN_MAX_SECONDS in scripts/schedule_state.py, the single source of
|
|
2993
|
+
// truth every Python stall detector derives from; this TS constant can't import
|
|
2994
|
+
// it, so keep it in sync by hand when retuning there.
|
|
2995
|
+
const AUTOPILOT_STALL_MS = 1_200_000;
|
|
2991
2996
|
// True when no scheduled-task routine is draining the draft queue. Two signals,
|
|
2992
2997
|
// OR'd (keep in lockstep with mcp/menubar/s4l_menubar.py::_autopilot_stalled and
|
|
2993
2998
|
// scripts/autopilot_stall_watch.py):
|
|
@@ -3500,6 +3505,9 @@ async function ensureQueueKickerInstalled() {
|
|
|
3500
3505
|
stdoutLog: path.join(logDir, "launchd-twitter-cycle-stdout.log"),
|
|
3501
3506
|
stderrLog: path.join(logDir, "launchd-twitter-cycle-stderr.log"),
|
|
3502
3507
|
extraEnv: kickerEnv(),
|
|
3508
|
+
// Don't let launchd reap the harness Chrome the cycle launches when the
|
|
3509
|
+
// kicker shell exits (2026-07-12 foreground-steal loop).
|
|
3510
|
+
abandonProcessGroup: true,
|
|
3503
3511
|
});
|
|
3504
3512
|
// Content-aware install: an existing box has the OLD kicker plist pointing at
|
|
3505
3513
|
// run-twitter-cycle.sh (no merge step). ensurePlist won't overwrite, so detect
|
|
@@ -5101,6 +5109,17 @@ async function main() {
|
|
|
5101
5109
|
if (ensureRuntimeProvisioned()) {
|
|
5102
5110
|
console.error("[social-autoposter-mcp] owned runtime not ready; provisioning on boot");
|
|
5103
5111
|
}
|
|
5112
|
+
// Vendored harness patch for ALREADY-ready runtimes: provision() is skipped
|
|
5113
|
+
// above when the runtime is ready, so a patch shipped in an rc would never
|
|
5114
|
+
// reach an existing install's harness checkout without this. Idempotent
|
|
5115
|
+
// (marker check) and best-effort; never blocks boot.
|
|
5116
|
+
void ensureHarnessPatched()
|
|
5117
|
+
.then((r) => {
|
|
5118
|
+
if (!(r.ok && r.detail === "already patched")) {
|
|
5119
|
+
console.error(`[social-autoposter-mcp] harness patch: ${r.ok ? "ok" : "skip"} (${r.detail})`);
|
|
5120
|
+
}
|
|
5121
|
+
})
|
|
5122
|
+
.catch(() => { });
|
|
5104
5123
|
// Queue-backed drafting (2026-06-23): keep the two worker-task prompts current,
|
|
5105
5124
|
// pre-approve their tools EAGERLY (before onboarding even creates the tasks, so
|
|
5106
5125
|
// the first unattended fire can't stall), and (re)install the launchd kicker
|
package/mcp/dist/runtime.js
CHANGED
|
@@ -239,6 +239,30 @@ function bundledVersion() {
|
|
|
239
239
|
return null;
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
|
+
// `ensurePipelineCurrent()` extracts updates over the existing materialized
|
|
243
|
+
// repo so customer config and logs survive. That also means deleting a file
|
|
244
|
+
// from the npm tarball does not delete an older copy already on disk. Keep
|
|
245
|
+
// explicit tombstones for retired helpers long enough for the fleet to pass
|
|
246
|
+
// through this release; new installs never receive these files.
|
|
247
|
+
const RETIRED_MATERIALIZED_FILES = [
|
|
248
|
+
"scripts/saps_mode.py",
|
|
249
|
+
"scripts/saps_activity.py",
|
|
250
|
+
"scripts/_lock_preempt_test.py",
|
|
251
|
+
];
|
|
252
|
+
function removeRetiredMaterializedFiles() {
|
|
253
|
+
for (const rel of RETIRED_MATERIALIZED_FILES) {
|
|
254
|
+
const target = path.join(MATERIALIZED_REPO, rel);
|
|
255
|
+
try {
|
|
256
|
+
if (!fs.existsSync(target))
|
|
257
|
+
continue;
|
|
258
|
+
fs.rmSync(target, { force: true });
|
|
259
|
+
console.error(`[runtime] removed retired pipeline file ${rel}`);
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
console.error(`[runtime] could not remove retired pipeline file ${rel}: ${e?.message || e}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
242
266
|
// Re-materialize the pipeline source when a plugin UPDATE shipped a newer
|
|
243
267
|
// pipeline.tgz than what's on disk. The .mcpb update refreshes dist/ (this
|
|
244
268
|
// server) but does NOT re-extract the embedded tarball, so without this the box
|
|
@@ -269,6 +293,9 @@ export function ensurePipelineCurrent() {
|
|
|
269
293
|
const bundled = bundledVersion();
|
|
270
294
|
if (!bundled)
|
|
271
295
|
return; // dev build, no stamp — leave the materialized repo alone.
|
|
296
|
+
// Run before the version early-return too: a prior package build may have
|
|
297
|
+
// accidentally included a retired file under this same version.
|
|
298
|
+
removeRetiredMaterializedFiles();
|
|
272
299
|
const rt = readRuntime();
|
|
273
300
|
if (rt?.pipeline_version === bundled)
|
|
274
301
|
return; // already current.
|
|
@@ -283,6 +310,9 @@ export function ensurePipelineCurrent() {
|
|
|
283
310
|
console.error(`[runtime] pipeline re-materialize failed (exit ${r.status}); keeping existing pipeline`);
|
|
284
311
|
return;
|
|
285
312
|
}
|
|
313
|
+
// Defense in depth: a stale/locally-built tarball cannot reintroduce a
|
|
314
|
+
// retired helper even if its package allowlist drifts.
|
|
315
|
+
removeRetiredMaterializedFiles();
|
|
286
316
|
// Record the new version so we don't re-extract on every boot.
|
|
287
317
|
const next = rt ?? readRuntime();
|
|
288
318
|
if (next) {
|
|
@@ -589,6 +619,55 @@ export function ensureRuntimeProvisioned() {
|
|
|
589
619
|
return false; // best-effort; a boot provision must never break startup
|
|
590
620
|
}
|
|
591
621
|
}
|
|
622
|
+
// Boot hook closing the provisioning gap for ALREADY-ready runtimes: provision()
|
|
623
|
+
// skips when runtimeReady(), so a vendored-patch update shipped in an rc never
|
|
624
|
+
// reaches an existing install's harness checkout (proved on the QA box: rc.26
|
|
625
|
+
// delivered scripts/patches/ but the checkout stayed unpatched, 2026-07-13).
|
|
626
|
+
// This runs on every MCP boot: if the fix marker is absent and the patch
|
|
627
|
+
// applies, apply it and reload the harness daemon so the running process picks
|
|
628
|
+
// up the new source (the uv tool install is EDITABLE, so patched files are
|
|
629
|
+
// live without a reinstall). Best-effort; never blocks startup.
|
|
630
|
+
export async function ensureHarnessPatched() {
|
|
631
|
+
try {
|
|
632
|
+
// Version marker of the CURRENT vendored patch: a symbol that exists only
|
|
633
|
+
// in its newest revision (v2 added the daemon.py websocket-proxy
|
|
634
|
+
// neutralization after v1's cdp_urlopen alone proved insufficient — the
|
|
635
|
+
// WS dial still routed through the macOS system proxy and died with
|
|
636
|
+
// "proxy rejected connection: HTTP 503"). Bump this string whenever the
|
|
637
|
+
// patch gains a new hunk, or upgraded installs will silently keep the
|
|
638
|
+
// previous revision.
|
|
639
|
+
const daemonPy = path.join(HARNESS_DIR, "src", "browser_harness", "daemon.py");
|
|
640
|
+
if (!fs.existsSync(daemonPy))
|
|
641
|
+
return { ok: false, detail: "harness not installed yet" };
|
|
642
|
+
if (fs.readFileSync(daemonPy, "utf-8").includes("s4l_no_proxy_ws")) {
|
|
643
|
+
return { ok: true, detail: "already patched" };
|
|
644
|
+
}
|
|
645
|
+
const harnessPatch = path.join(MATERIALIZED_REPO, "scripts", "patches", "browser-harness-loopback-cdp-proxy.patch");
|
|
646
|
+
if (!fs.existsSync(harnessPatch))
|
|
647
|
+
return { ok: false, detail: "patch file missing from package" };
|
|
648
|
+
// Discard any PRIOR vendored-patch revision (e.g. v1) so the cumulative
|
|
649
|
+
// patch applies onto pristine pinned sources. Only src/ is touched; the
|
|
650
|
+
// checkout is a managed artifact, never a place for local edits.
|
|
651
|
+
await sh("git", ["-C", HARNESS_DIR, "checkout", "--", "src/browser_harness"], {
|
|
652
|
+
timeoutMs: 30000,
|
|
653
|
+
});
|
|
654
|
+
const chk = await sh("git", ["-C", HARNESS_DIR, "apply", "--check", harnessPatch], {
|
|
655
|
+
timeoutMs: 30000,
|
|
656
|
+
});
|
|
657
|
+
if (chk.code !== 0) {
|
|
658
|
+
return { ok: false, detail: "patch does not apply (dirty or diverged checkout)" };
|
|
659
|
+
}
|
|
660
|
+
const app = await sh("git", ["-C", HARNESS_DIR, "apply", harnessPatch], { timeoutMs: 30000 });
|
|
661
|
+
if (app.code !== 0)
|
|
662
|
+
return { ok: false, detail: `git apply failed (exit ${app.code})` };
|
|
663
|
+
// Reload the daemon so the long-lived process re-imports the patched source.
|
|
664
|
+
await sh(HARNESS_BIN, ["--reload"], { timeoutMs: 30000 });
|
|
665
|
+
return { ok: true, detail: "patched + daemon reloaded" };
|
|
666
|
+
}
|
|
667
|
+
catch (e) {
|
|
668
|
+
return { ok: false, detail: String(e?.message || e) };
|
|
669
|
+
}
|
|
670
|
+
}
|
|
592
671
|
async function provision(progress) {
|
|
593
672
|
const setStep = (id, status, detail) => {
|
|
594
673
|
const st = progress.steps.find((s) => s.id === id);
|
|
@@ -828,6 +907,37 @@ async function provision(progress) {
|
|
|
828
907
|
timeoutMs: 120000,
|
|
829
908
|
});
|
|
830
909
|
await sh("git", ["-C", HARNESS_DIR, "reset", "--hard", "FETCH_HEAD"], { timeoutMs: 60000 });
|
|
910
|
+
// Vendored fix on top of the pin: loopback CDP requests must never route
|
|
911
|
+
// through a proxy. macOS system proxy settings leak into urllib's default
|
|
912
|
+
// opener, and a box-wide forwarder 403s every 127.0.0.1 probe, so Chrome
|
|
913
|
+
// reads as "wedged"/logged-out while it is actually fine (2026-07-13).
|
|
914
|
+
// Upstream doesn't carry the fix and we don't control that repo, so apply
|
|
915
|
+
// our patch at install time, on every machine. If it stops applying
|
|
916
|
+
// cleanly, upstream has either merged the fix (detected below; fine) or
|
|
917
|
+
// refactored the files (warn); never fail the install over it.
|
|
918
|
+
const harnessPatch = path.join(MATERIALIZED_REPO, "scripts", "patches", "browser-harness-loopback-cdp-proxy.patch");
|
|
919
|
+
if (fs.existsSync(harnessPatch)) {
|
|
920
|
+
const chk = await sh("git", ["-C", HARNESS_DIR, "apply", "--check", harnessPatch], {
|
|
921
|
+
timeoutMs: 30000,
|
|
922
|
+
});
|
|
923
|
+
if (chk.code === 0) {
|
|
924
|
+
await sh("git", ["-C", HARNESS_DIR, "apply", harnessPatch], { timeoutMs: 30000 });
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
let fixedUpstream = false;
|
|
928
|
+
try {
|
|
929
|
+
fixedUpstream = fs
|
|
930
|
+
.readFileSync(path.join(HARNESS_DIR, "src", "browser_harness", "admin.py"), "utf-8")
|
|
931
|
+
.includes("cdp_urlopen");
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
/* file moved — fall through to the warning */
|
|
935
|
+
}
|
|
936
|
+
if (!fixedUpstream) {
|
|
937
|
+
console.error("[runtime] browser-harness loopback-proxy patch no longer applies and the fix is absent upstream; CDP probes may 403 behind a system proxy");
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
831
941
|
// Install the CLI via uv tool (lands at ~/.local/bin/browser-harness).
|
|
832
942
|
// --force so a refreshed source / changed entry point is reinstalled.
|
|
833
943
|
const inst = await sh(uv, ["tool", "install", "--force", "-e", HARNESS_DIR], {
|