social-autoposter 1.6.164 → 1.6.166
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 +1 -1
- package/bin/cli.js +3 -96
- package/bin/platform.js +0 -5
- package/bin/scheduler/index.js +1 -3
- package/mcp/dist/index.js +95 -3
- package/mcp/dist/panel.html +1 -1
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +2 -2
- package/mcp/menubar/s4l_menubar.py +1 -1
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/harness_overlay.py +15 -0
- package/setup/SKILL.md +21 -9
- package/skill/run-overlay-watch.sh +15 -1
- package/bin/scheduler/systemd.js +0 -313
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ npx social-autoposter update
|
|
|
45
45
|
|
|
46
46
|
## Configure
|
|
47
47
|
|
|
48
|
-
Tell your Claude agent: **"set me up on social-autoposter end to end"**. The
|
|
48
|
+
Tell your Claude agent: **"set me up on social-autoposter plugin end to end"**. The
|
|
49
49
|
setup skill treats that as a terminal goal:
|
|
50
50
|
|
|
51
51
|
1. Inspect and repair the owned runtime.
|
package/bin/cli.js
CHANGED
|
@@ -6,7 +6,6 @@ const fs = require('fs');
|
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const { spawnSync } = require('child_process');
|
|
8
8
|
|
|
9
|
-
const platform = require('./platform');
|
|
10
9
|
const scheduler = require('./scheduler');
|
|
11
10
|
const { formatDoctorReport, runDoctorSync } = require('../mcp/shared/doctor.cjs');
|
|
12
11
|
const { recordDoctorReport } = require('../mcp/shared/onboarding-ledger.cjs');
|
|
@@ -610,88 +609,9 @@ function generatePlists() {
|
|
|
610
609
|
|
|
611
610
|
const driver = scheduler.driverFor();
|
|
612
611
|
const env = driver.defaultEnv({ home: HOME, nodeBin });
|
|
613
|
-
const
|
|
614
|
-
const outDir = path.join(DEST, kind === 'systemd' ? 'systemd' : 'launchd');
|
|
612
|
+
const outDir = path.join(DEST, 'launchd');
|
|
615
613
|
driver.generate({ jobs, outDir, env });
|
|
616
|
-
console.log(` generated
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// On Linux we translate every shipped launchd plist into a systemd
|
|
620
|
-
// .service + .timer pair at install time. Plists remain the source of truth
|
|
621
|
-
// so the macOS pipeline is untouched; the systemd/ dir is derived.
|
|
622
|
-
function generateSystemdFromPlists() {
|
|
623
|
-
const launchdDriver = scheduler.driverFor('launchd');
|
|
624
|
-
const systemdDriver = scheduler.driverFor('systemd');
|
|
625
|
-
const srcDir = path.join(DEST, 'launchd');
|
|
626
|
-
const outDir = path.join(DEST, 'systemd');
|
|
627
|
-
if (!fs.existsSync(srcDir)) return 0;
|
|
628
|
-
const plists = fs.readdirSync(srcDir).filter(f => f.endsWith('.plist'));
|
|
629
|
-
const nodeBin = path.dirname(process.execPath);
|
|
630
|
-
const env = systemdDriver.defaultEnv({ home: HOME, nodeBin });
|
|
631
|
-
|
|
632
|
-
const jobs = [];
|
|
633
|
-
let skipped = 0;
|
|
634
|
-
for (const f of plists) {
|
|
635
|
-
const xml = fs.readFileSync(path.join(srcDir, f), 'utf8');
|
|
636
|
-
const { label, scriptPath } = launchdDriver.parseUnit(xml);
|
|
637
|
-
if (!label || !scriptPath) { skipped++; continue; }
|
|
638
|
-
const sched = launchdDriver.scheduleFromUnit(xml);
|
|
639
|
-
if (!sched.intervalSecs) {
|
|
640
|
-
console.log(` skip ${f}: calendar schedule not yet translated to OnCalendar`);
|
|
641
|
-
skipped++;
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
// Plists ship with the publisher's absolute paths baked in. Rebuild
|
|
645
|
-
// paths against the current user's DEST so any user on any host gets
|
|
646
|
-
// correct units without us having to re-ship plists per install target.
|
|
647
|
-
const scriptBase = path.basename(scriptPath);
|
|
648
|
-
const stdoutMatch = (xml.match(/<key>StandardOutPath<\/key>\s*<string>([^<]+)<\/string>/) || [])[1];
|
|
649
|
-
const stderrMatch = (xml.match(/<key>StandardErrorPath<\/key>\s*<string>([^<]+)<\/string>/) || [])[1];
|
|
650
|
-
const shortLabel = label.replace(/^com\.m13v\.social-/, '');
|
|
651
|
-
const stdout = `${DEST}/skill/logs/${stdoutMatch ? path.basename(stdoutMatch) : `launchd-${shortLabel}-stdout.log`}`;
|
|
652
|
-
const stderr = `${DEST}/skill/logs/${stderrMatch ? path.basename(stderrMatch) : `launchd-${shortLabel}-stderr.log`}`;
|
|
653
|
-
const runAtLoad = /<key>RunAtLoad<\/key>\s*<true\s*\/>/.test(xml);
|
|
654
|
-
jobs.push({
|
|
655
|
-
file: f,
|
|
656
|
-
label,
|
|
657
|
-
script: `${DEST}/skill/${scriptBase}`,
|
|
658
|
-
interval: sched.intervalSecs,
|
|
659
|
-
runAtLoad,
|
|
660
|
-
stdoutLog: stdout,
|
|
661
|
-
stderrLog: stderr,
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
systemdDriver.generate({ jobs, outDir, env });
|
|
665
|
-
console.log(` translated ${jobs.length} launchd plists -> systemd units (skipped ${skipped})`);
|
|
666
|
-
return jobs.length;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// Link every DEST/systemd/*.{service,timer} into ~/.config/systemd/user/ and
|
|
670
|
-
// reload the user daemon. Caller is expected to `systemctl --user enable --now
|
|
671
|
-
// <timer>` for each timer they actually want running; this mirrors how macOS
|
|
672
|
-
// setup leaves loading to the user via the SKILL.md wizard.
|
|
673
|
-
function installSystemdUnits() {
|
|
674
|
-
const driver = scheduler.driverFor('systemd');
|
|
675
|
-
const unitDir = path.join(DEST, 'systemd');
|
|
676
|
-
const agentsDir = platform.agentsDir();
|
|
677
|
-
if (!fs.existsSync(unitDir)) return;
|
|
678
|
-
fs.mkdirSync(agentsDir, { recursive: true });
|
|
679
|
-
const services = fs.readdirSync(unitDir).filter(f => f.endsWith('.service'));
|
|
680
|
-
let linked = 0;
|
|
681
|
-
for (const f of services) {
|
|
682
|
-
if (driver.install(path.join(unitDir, f), agentsDir)) linked++;
|
|
683
|
-
}
|
|
684
|
-
const r = spawnSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf8' });
|
|
685
|
-
if (r.status === 0) {
|
|
686
|
-
console.log(` linked ${linked} unit pair(s) into ${agentsDir}; systemctl --user daemon-reload OK`);
|
|
687
|
-
} else {
|
|
688
|
-
console.warn(` linked ${linked} unit pair(s); daemon-reload failed: ${(r.stderr || '').trim()}`);
|
|
689
|
-
}
|
|
690
|
-
const linger = spawnSync('loginctl', ['show-user', os.userInfo().username, '--property=Linger'], { encoding: 'utf8' });
|
|
691
|
-
if (!/Linger=yes/.test(linger.stdout || '')) {
|
|
692
|
-
console.log(' note: run `sudo loginctl enable-linger $USER` so timers fire when nobody is logged in');
|
|
693
|
-
}
|
|
694
|
-
console.log(' next: systemctl --user enable --now <timer> for each job you want scheduled');
|
|
614
|
+
console.log(` generated launchd units at ${outDir}`);
|
|
695
615
|
}
|
|
696
616
|
|
|
697
617
|
function init() {
|
|
@@ -715,13 +635,6 @@ function init() {
|
|
|
715
635
|
// Generate launchd plists with user's actual HOME
|
|
716
636
|
generatePlists();
|
|
717
637
|
|
|
718
|
-
// On Linux, derive systemd units from every plist and link them into
|
|
719
|
-
// ~/.config/systemd/user/. macOS install is unchanged.
|
|
720
|
-
if (platform.scheduler() === 'systemd') {
|
|
721
|
-
generateSystemdFromPlists();
|
|
722
|
-
installSystemdUnits();
|
|
723
|
-
}
|
|
724
|
-
|
|
725
638
|
// Provision the browser-harness toolchain BEFORE writing harness configs so
|
|
726
639
|
// findUvBin() picks up a freshly-installed uv on first run.
|
|
727
640
|
installBrowserHarness();
|
|
@@ -768,7 +681,7 @@ function init() {
|
|
|
768
681
|
console.log('');
|
|
769
682
|
console.log('Done! Next steps:');
|
|
770
683
|
console.log(' 1. Fully quit and relaunch Claude so the MCP loads');
|
|
771
|
-
console.log(' 2. Tell your Claude agent: "set me up on social-autoposter end to end"');
|
|
684
|
+
console.log(' 2. Tell your Claude agent: "set me up on social-autoposter plugin end to end"');
|
|
772
685
|
console.log(' The agent will configure the product, connect X, seed topics, and verify a draft cycle');
|
|
773
686
|
console.log(' 3. Posts and all pipeline state sync via the s4l.ai HTTP API (no Postgres required)');
|
|
774
687
|
}
|
|
@@ -801,12 +714,6 @@ function update() {
|
|
|
801
714
|
// Regenerate launchd plists with correct paths
|
|
802
715
|
generatePlists();
|
|
803
716
|
|
|
804
|
-
// Refresh systemd units on Linux so plist changes propagate.
|
|
805
|
-
if (platform.scheduler() === 'systemd') {
|
|
806
|
-
generateSystemdFromPlists();
|
|
807
|
-
installSystemdUnits();
|
|
808
|
-
}
|
|
809
|
-
|
|
810
717
|
// Provision browser-harness (uv + clone + uv tool install + mcp pkg + server.py).
|
|
811
718
|
// Idempotent: skips steps that are already done.
|
|
812
719
|
installBrowserHarness();
|
package/bin/platform.js
CHANGED
|
@@ -7,31 +7,26 @@ const fs = require('fs');
|
|
|
7
7
|
function detect() {
|
|
8
8
|
const p = process.platform;
|
|
9
9
|
if (p === 'darwin') return 'darwin';
|
|
10
|
-
if (p === 'linux') return 'linux';
|
|
11
10
|
return p;
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
function scheduler(platform = detect()) {
|
|
15
14
|
if (platform === 'darwin') return 'launchd';
|
|
16
|
-
if (platform === 'linux') return 'systemd';
|
|
17
15
|
return null;
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
function agentsDir(platform = detect(), home = os.homedir()) {
|
|
21
19
|
if (platform === 'darwin') return path.join(home, 'Library', 'LaunchAgents');
|
|
22
|
-
if (platform === 'linux') return path.join(home, '.config', 'systemd', 'user');
|
|
23
20
|
return null;
|
|
24
21
|
}
|
|
25
22
|
|
|
26
23
|
function statMtimeCmd(platform = detect()) {
|
|
27
24
|
if (platform === 'darwin') return ['stat', '-f', '%m'];
|
|
28
|
-
if (platform === 'linux') return ['stat', '-c', '%Y'];
|
|
29
25
|
return null;
|
|
30
26
|
}
|
|
31
27
|
|
|
32
28
|
function notifier(platform = detect()) {
|
|
33
29
|
if (platform === 'darwin') return 'osascript';
|
|
34
|
-
if (platform === 'linux') return 'notify-send';
|
|
35
30
|
return null;
|
|
36
31
|
}
|
|
37
32
|
|
package/bin/scheduler/index.js
CHANGED
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const platform = require('../platform');
|
|
4
4
|
const launchd = require('./launchd');
|
|
5
|
-
const systemd = require('./systemd');
|
|
6
5
|
|
|
7
6
|
function driverFor(name) {
|
|
8
7
|
const key = name || platform.scheduler();
|
|
9
8
|
if (key === 'launchd') return launchd;
|
|
10
|
-
if (key === 'systemd') return systemd;
|
|
11
9
|
throw new Error(`no scheduler driver for: ${key}`);
|
|
12
10
|
}
|
|
13
11
|
|
|
14
|
-
module.exports = { driverFor, launchd
|
|
12
|
+
module.exports = { driverFor, launchd };
|
package/mcp/dist/index.js
CHANGED
|
@@ -82,6 +82,21 @@ const MEMORY_SNAPSHOT_INTERVAL_SECS = 60;
|
|
|
82
82
|
const STALL_WATCH_LABEL = "com.m13v.social-autopilot-stall-watch";
|
|
83
83
|
const STALL_WATCH_PLIST = path.join(os.homedir(), "Library", "LaunchAgents", `${STALL_WATCH_LABEL}.plist`);
|
|
84
84
|
const STALL_WATCH_INTERVAL_SECS = 120;
|
|
85
|
+
// On-screen overlay watcher. The harness status overlay ("S4L running" / idle
|
|
86
|
+
// banner) only renders WHILE `harness_overlay.py watch` is alive. That watcher
|
|
87
|
+
// is fire-and-forget with no supervisor of its own, so when it dies (or the
|
|
88
|
+
// harness Chrome restarts) nothing brings it back and the overlay silently
|
|
89
|
+
// disappears. Promote it to a first-class launchd job, but run the long-lived
|
|
90
|
+
// watcher in the FOREGROUND under KeepAlive (NOT a StartInterval that re-invokes
|
|
91
|
+
// a spawn-and-exit supervisor). The supervisor pattern races launchd on macOS:
|
|
92
|
+
// the instant the kicker shell exits, launchd SIGKILLs the whole job process
|
|
93
|
+
// group and reaps the just-spawned watcher before it can detach. Running the
|
|
94
|
+
// watcher AS the job's main process makes launchd supervise it directly:
|
|
95
|
+
// RunAtLoad starts it at boot, KeepAlive restarts it if it ever exits, and on
|
|
96
|
+
// unload its SIGTERM handler clears the overlay cleanly. Disable with
|
|
97
|
+
// SAPS_OVERLAY_WATCH=0.
|
|
98
|
+
const OVERLAY_WATCH_LABEL = "com.m13v.social-overlay-watch";
|
|
99
|
+
const OVERLAY_WATCH_PLIST = path.join(os.homedir(), "Library", "LaunchAgents", `${OVERLAY_WATCH_LABEL}.plist`);
|
|
85
100
|
// Daily self-updater. Enabled alongside autopilot so a hands-free (headless)
|
|
86
101
|
// install keeps itself current — the interactive `runtime` tool (action:'update')
|
|
87
102
|
// only helps when
|
|
@@ -123,6 +138,9 @@ function launchdPath() {
|
|
|
123
138
|
}
|
|
124
139
|
function plistXml(opts) {
|
|
125
140
|
const args = opts.programArgs.map((a) => `\t\t<string>${a}</string>`).join("\n");
|
|
141
|
+
const schedule = opts.keepAlive
|
|
142
|
+
? `\t<key>KeepAlive</key>\n\t<true/>`
|
|
143
|
+
: `\t<key>StartInterval</key>\n\t<integer>${opts.intervalSecs}</integer>`;
|
|
126
144
|
// Background (cron/autopilot) runs get the same Chrome the interactive cycle
|
|
127
145
|
// uses, so a no-sudo ~/Applications install (which the shell's own resolver
|
|
128
146
|
// doesn't scan) is still found off-screen. Omitted when Chrome resolves via
|
|
@@ -148,8 +166,7 @@ function plistXml(opts) {
|
|
|
148
166
|
\t<array>
|
|
149
167
|
${args}
|
|
150
168
|
\t</array>
|
|
151
|
-
|
|
152
|
-
\t<integer>${opts.intervalSecs}</integer>
|
|
169
|
+
${schedule}
|
|
153
170
|
\t<key>StandardOutPath</key>
|
|
154
171
|
\t<string>${opts.stdoutLog}</string>
|
|
155
172
|
\t<key>StandardErrorPath</key>
|
|
@@ -969,7 +986,7 @@ server.registerPrompt("getting-started", {
|
|
|
969
986
|
role: "user",
|
|
970
987
|
content: {
|
|
971
988
|
type: "text",
|
|
972
|
-
text: "Set up social-autoposter end to end now. Treat this as a terminal goal: inspect status, " +
|
|
989
|
+
text: "Set up social-autoposter plugin end to end now. Treat this as a terminal goal: inspect status, " +
|
|
973
990
|
"install or repair the owned runtime, auto-detect and connect my X session, scan my " +
|
|
974
991
|
"profile, discover and research the product I most clearly represent, infer and save a " +
|
|
975
992
|
"conservative complete project with search topics, seed them, and run a draft-only " +
|
|
@@ -2684,6 +2701,73 @@ async function ensureMemorySnapshotInstalled() {
|
|
|
2684
2701
|
return { ok: false, detail: e?.message || String(e) };
|
|
2685
2702
|
}
|
|
2686
2703
|
}
|
|
2704
|
+
// Install/refresh the on-screen overlay watcher launchd job. Promotes the
|
|
2705
|
+
// harness status overlay from a best-effort, fired-from-other-tools nicety to a
|
|
2706
|
+
// first-class self-healing job. We run `harness_overlay.py watch` directly in
|
|
2707
|
+
// the FOREGROUND under KeepAlive (RunAtLoad starts it at boot; launchd restarts
|
|
2708
|
+
// it if it ever exits) rather than a StartInterval that re-fires a spawn-and-exit
|
|
2709
|
+
// supervisor: on macOS that supervisor races launchd, which SIGKILLs the job's
|
|
2710
|
+
// process group the instant the kicker shell exits and reaps the just-spawned
|
|
2711
|
+
// watcher before it can detach (verified on the box: the watcher caught the
|
|
2712
|
+
// group SIGTERM and cleared the overlay every cycle). harness_overlay.py holds a
|
|
2713
|
+
// singleton flock so the MCP's best-effort run-overlay-watch.sh lane can never
|
|
2714
|
+
// double-paint. SAPS_PYTHON is baked by plistXml; we add SAPS_LOG_DIR (so the
|
|
2715
|
+
// watcher reads the same cycle logs to decide busy/idle) and the harness CDP
|
|
2716
|
+
// URL. Disable with SAPS_OVERLAY_WATCH=0.
|
|
2717
|
+
async function ensureOverlayWatchInstalled() {
|
|
2718
|
+
try {
|
|
2719
|
+
if (process.platform !== "darwin")
|
|
2720
|
+
return { ok: false, detail: "not macOS" };
|
|
2721
|
+
if (process.env.SAPS_OVERLAY_WATCH === "0")
|
|
2722
|
+
return { ok: false, detail: "disabled (SAPS_OVERLAY_WATCH=0)" };
|
|
2723
|
+
const logDir = path.join(repoDir(), "skill", "logs");
|
|
2724
|
+
try {
|
|
2725
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
2726
|
+
}
|
|
2727
|
+
catch {
|
|
2728
|
+
/* best-effort */
|
|
2729
|
+
}
|
|
2730
|
+
const xml = plistXml({
|
|
2731
|
+
label: OVERLAY_WATCH_LABEL,
|
|
2732
|
+
programArgs: [resolvePython(), path.join(repoDir(), "scripts", "harness_overlay.py"), "watch"],
|
|
2733
|
+
intervalSecs: 0,
|
|
2734
|
+
keepAlive: true,
|
|
2735
|
+
runAtLoad: true,
|
|
2736
|
+
stdoutLog: path.join(logDir, "launchd-overlay-watch-stdout.log"),
|
|
2737
|
+
stderrLog: path.join(logDir, "launchd-overlay-watch-stderr.log"),
|
|
2738
|
+
extraEnv: {
|
|
2739
|
+
SAPS_LOG_DIR: logDir,
|
|
2740
|
+
TWITTER_CDP_URL: process.env.TWITTER_CDP_URL || "http://127.0.0.1:9555",
|
|
2741
|
+
},
|
|
2742
|
+
});
|
|
2743
|
+
const uid = process.getuid ? process.getuid() : 0;
|
|
2744
|
+
let cur = null;
|
|
2745
|
+
try {
|
|
2746
|
+
cur = fs.readFileSync(OVERLAY_WATCH_PLIST, "utf-8");
|
|
2747
|
+
}
|
|
2748
|
+
catch {
|
|
2749
|
+
cur = null;
|
|
2750
|
+
}
|
|
2751
|
+
let detail;
|
|
2752
|
+
if (cur === xml) {
|
|
2753
|
+
const res = await loadPlist(OVERLAY_WATCH_LABEL, OVERLAY_WATCH_PLIST, uid);
|
|
2754
|
+
detail = `current (load rc=${res.code})`;
|
|
2755
|
+
}
|
|
2756
|
+
else {
|
|
2757
|
+
if (cur !== null) {
|
|
2758
|
+
await unloadPlist(OVERLAY_WATCH_LABEL, OVERLAY_WATCH_PLIST, uid);
|
|
2759
|
+
}
|
|
2760
|
+
fs.mkdirSync(path.dirname(OVERLAY_WATCH_PLIST), { recursive: true });
|
|
2761
|
+
fs.writeFileSync(OVERLAY_WATCH_PLIST, xml, "utf-8");
|
|
2762
|
+
const res = await loadPlist(OVERLAY_WATCH_LABEL, OVERLAY_WATCH_PLIST, uid);
|
|
2763
|
+
detail = cur === null ? "installed + loaded" : `rewritten + reloaded (rc=${res.code})`;
|
|
2764
|
+
}
|
|
2765
|
+
return { ok: true, detail };
|
|
2766
|
+
}
|
|
2767
|
+
catch (e) {
|
|
2768
|
+
return { ok: false, detail: e?.message || String(e) };
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2687
2771
|
// Is the draft schedule registered AND running for the LIVE account?
|
|
2688
2772
|
// 'ok' — worker tasks present+enabled and FIRING (host actively running).
|
|
2689
2773
|
// 'disabled' — present but a worker task is disabled.
|
|
@@ -3586,6 +3670,14 @@ async function main() {
|
|
|
3586
3670
|
void ensureMemorySnapshotInstalled()
|
|
3587
3671
|
.then((r) => console.error(`[memory-snapshot] launchd sampler: ${r.ok ? "ok" : "skip"} (${r.detail})`))
|
|
3588
3672
|
.catch((e) => console.error("[memory-snapshot] sampler install failed:", e?.message || e));
|
|
3673
|
+
// On-screen overlay watcher supervisor. The harness status overlay only renders
|
|
3674
|
+
// while the watcher process is alive, and that watcher had no supervisor — when
|
|
3675
|
+
// it died nothing respawned it and the overlay silently vanished. Install it as
|
|
3676
|
+
// a first-class self-healing launchd job (RunAtLoad + 60s idempotent re-invoke).
|
|
3677
|
+
// Best-effort; the overlay is a nicety and must never block boot.
|
|
3678
|
+
void ensureOverlayWatchInstalled()
|
|
3679
|
+
.then((r) => console.error(`[overlay-watch] launchd supervisor: ${r.ok ? "ok" : "skip"} (${r.detail})`))
|
|
3680
|
+
.catch((e) => console.error("[overlay-watch] supervisor install failed:", e?.message || e));
|
|
3589
3681
|
// Heal installs onboarded before short_links_live defaulted to false: such a
|
|
3590
3682
|
// project wraps short links against the customer's own domain, which has no
|
|
3591
3683
|
// /r/[code] resolver, so every minted link 404s. Re-point them at the s4l.ai
|
package/mcp/dist/panel.html
CHANGED
|
@@ -70,7 +70,7 @@ Boolean requesting whether a visible border and background is provided by the ho
|
|
|
70
70
|
- omitted: host decides border`)});m({method:u("ui/request-display-mode"),params:m({mode:et.describe("The display mode being requested.")})});var ph=m({mode:et.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),fh=U([u("model"),u("app")]).describe("Tool visibility scope - who can access the tool.");m({resourceUri:d().optional(),visibility:z(fh).optional().describe(`Who can access this tool. Default: ["model", "app"]
|
|
71
71
|
- "model": Tool visible to and callable by the agent
|
|
72
72
|
- "app": Tool callable by the app from this server only`),csp:Ie().optional(),permissions:Ie().optional()});m({mimeTypes:z(d()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')});m({method:u("ui/download-file"),params:m({contents:z(U([hl,gl])).describe("Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types.")})});m({method:u("ui/message"),params:m({role:u("user").describe('Message role, currently only "user" is supported.'),content:z($t).describe("Message content blocks (text, image, etc.).")})});m({method:u("ui/notifications/sandbox-resource-ready"),params:m({html:d().describe("HTML content to load into the inner iframe."),sandbox:d().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:go.optional().describe("CSP configuration from resource metadata."),permissions:vo.optional().describe("Sandbox permissions from resource metadata.")})});var hh=m({method:u("ui/notifications/tool-result"),params:Fn.describe("Standard MCP tool execution result.")}),kl=m({toolInfo:m({id:ht.optional().describe("JSON-RPC id of the tools/call request."),tool:ho.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:Xf.optional().describe("Current color theme preference."),styles:uh.optional().describe("Style configuration for theming the app."),displayMode:et.optional().describe("How the UI is currently displayed."),availableDisplayModes:z(et).optional().describe("Display modes the host supports."),containerDimensions:U([m({height:T().describe("Fixed container height in pixels.")}),m({maxHeight:U([T(),Qe()]).optional().describe("Maximum container height in pixels.")})]).and(U([m({width:T().describe("Fixed container width in pixels.")}),m({maxWidth:U([T(),Qe()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
|
|
73
|
-
container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:d().optional().describe("User's language and region preference in BCP 47 format."),timeZone:d().optional().describe("User's timezone in IANA format."),userAgent:d().optional().describe("Host application identifier."),platform:U([u("web"),u("desktop"),u("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:m({touch:M().optional().describe("Whether the device supports touch input."),hover:M().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:m({top:T().describe("Top safe area inset in pixels."),right:T().describe("Right safe area inset in pixels."),bottom:T().describe("Bottom safe area inset in pixels."),left:T().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),gh=m({method:u("ui/notifications/host-context-changed"),params:kl.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:z($t).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:Z(d(),q().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});m({method:u("ui/initialize"),params:m({appInfo:qn.describe("App identification (name and version)."),appCapabilities:mh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var vh=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:qn.describe("Host application identification and version."),hostCapabilities:dh.describe("Features and capabilities provided by the host."),hostContext:kl.describe("Rich context about the host environment.")}).passthrough(),_h={target:"draft-2020-12"};async function Fo(t,n){let r=t["~standard"];if(r.jsonSchema)return r.jsonSchema[n](_h);if(r.vendor==="zod"){let{z:o}=await El(()=>Promise.resolve().then(()=>op),void 0,import.meta.url);return o.toJSONSchema(t,{io:n})}throw Error(`Schema (vendor: ${r.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function Jo(t,n,r=""){let o=await t["~standard"].validate(n);if(o.issues){let e=o.issues.map(i=>{var s;let a=(s=i.path)==null?void 0:s.map(c=>typeof c=="object"?c.key:c).join(".");return a?`${a}: ${i.message}`:i.message}).join("; ");throw Error(r+e)}return o.value}function bh(t){let n=document.documentElement;n.setAttribute("data-theme",t),n.style.colorScheme=t}function yh(t,n=document.documentElement){for(let[r,o]of Object.entries(t))o!==void 0&&n.style.setProperty(r,o)}function $h(t){if(document.getElementById("__mcp-host-fonts"))return;let n=document.createElement("style");n.id="__mcp-host-fonts",n.textContent=t,document.head.appendChild(n)}const At=class At extends Kf{constructor(r,o={},e={autoResize:!0}){super(e);D(this,"_appInfo");D(this,"_capabilities");D(this,"options");D(this,"_hostCapabilities");D(this,"_hostInfo");D(this,"_hostContext");D(this,"_registeredTools",{});D(this,"_initializedSent",!1);D(this,"eventSchemas",{toolinput:oh,toolinputpartial:ah,toolresult:hh,toolcancelled:sh,hostcontextchanged:gh});D(this,"_everHadListener",new Set);D(this,"_toolHandlersInitialized",!1);D(this,"_onteardown");D(this,"_oncalltool");D(this,"_onlisttools");D(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=e,e.allowUnsafeEval||X({jitless:!0}),this.setRequestHandler(Hn,i=>(console.log("Received ping:",i.params),{})),this.setEventHandler("hostcontextchanged",void 0)}_assertInitialized(r){var e;if(this._initializedSent)return;let o=`[ext-apps] App.${r}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if((e=this.options)!=null&&e.strict)throw Error(o);console.warn(`${o}. This will throw in a future release.`)}_assertHandlerTiming(r){var e;if(!At.ONE_SHOT_EVENTS.has(r)||this._everHadListener.has(r)||(this._everHadListener.add(r),!this._initializedSent))return;let o=`[ext-apps] "${String(r)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if((e=this.options)!=null&&e.strict)throw Error(o);console.warn(o)}setEventHandler(r,o){o&&this._assertHandlerTiming(r),super.setEventHandler(r,o)}addEventListener(r,o){this._assertHandlerTiming(r),super.addEventListener(r,o)}onEventDispatch(r,o){r==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...o})}registerCapabilities(r){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._capabilities=Wf(this._capabilities,r)}registerTool(r,o,e){if(this._registeredTools[r])throw Error(`Tool ${r} is already registered`);let i=this,a=()=>{var p;i._initializedSent&&((p=i._capabilities.tools)!=null&&p.listChanged)&&i.sendToolListChanged()},s=o.inputSchema!==void 0,c={title:o.title,description:o.description,inputSchema:o.inputSchema,outputSchema:o.outputSchema,annotations:o.annotations,_meta:o._meta,enabled:!0,enable(){this.enabled=!0,a()},disable(){this.enabled=!1,a()},update(p){Object.assign(this,p),a()},remove(){i._registeredTools[r]===c&&(delete i._registeredTools[r],a())},handler:async(p,b)=>{if(!c.enabled)throw Error(`Tool ${r} is disabled`);let g;if(s){let $=c.inputSchema,w=$?await Jo($,p??{},`Invalid input for tool ${r}: `):p??{};g=await e(w,b)}else g=await e(b);return c.outputSchema&&!g.isError&&(g.structuredContent=await Jo(c.outputSchema,g.structuredContent,`Invalid output for tool ${r}: `)),g}};return this._registeredTools[r]=c,!this._capabilities.tools&&!this.transport&&this.registerCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),a(),c}ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(r,o)=>{let e=this._registeredTools[r.name];if(!e)throw Error(`Tool ${r.name} not found`);return e.handler(r.arguments,o)},this.onlisttools=async(r,o)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([e,i])=>i.enabled).map(async([e,i])=>{let a={name:e,title:i.title,description:i.description,inputSchema:i.inputSchema?await Fo(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await Fo(i.outputSchema,"output")),i.annotations&&(a.annotations=i.annotations),i._meta&&(a._meta=i._meta),a}))}))}async sendToolListChanged(r={}){this._assertInitialized("sendToolListChanged"),await this.notification({method:"notifications/tools/list_changed",params:r})}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}get ontoolinput(){return this.getEventHandler("toolinput")}set ontoolinput(r){this.setEventHandler("toolinput",r)}get ontoolinputpartial(){return this.getEventHandler("toolinputpartial")}set ontoolinputpartial(r){this.setEventHandler("toolinputpartial",r)}get ontoolresult(){return this.getEventHandler("toolresult")}set ontoolresult(r){this.setEventHandler("toolresult",r)}get ontoolcancelled(){return this.getEventHandler("toolcancelled")}set ontoolcancelled(r){this.setEventHandler("toolcancelled",r)}get onhostcontextchanged(){return this.getEventHandler("hostcontextchanged")}set onhostcontextchanged(r){this.setEventHandler("hostcontextchanged",r)}get onteardown(){return this._onteardown}set onteardown(r){this.warnIfRequestHandlerReplaced("onteardown",this._onteardown,r),this._onteardown=r,this.replaceRequestHandler(lh,(o,e)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(o.params,e)})}get oncalltool(){return this._oncalltool}set oncalltool(r){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,r),this._oncalltool=r,this.replaceRequestHandler(_l,(o,e)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(o.params,e)})}get onlisttools(){return this._onlisttools}set onlisttools(r){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,r),this._onlisttools=r,this.replaceRequestHandler(vl,(o,e)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(o.params,e)})}assertCapabilityForMethod(r){var o;switch(r){case"sampling/createMessage":if(!((o=this._hostCapabilities)!=null&&o.sampling))throw Error(`Host does not support sampling (required for ${r})`);break}}assertRequestHandlerCapability(r){switch(r){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${r})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${r} registered`)}}assertNotificationCapability(r){}assertTaskCapability(r){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(r){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(r,o){if(this._assertInitialized("callServerTool"),typeof r=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${r}"). Did you mean: callServerTool({ name: "${r}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:r},Fn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},fl,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},pl,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let e=r.tools?$l:yl;return await this.request({method:"sampling/createMessage",params:r},e,o)}sendMessage(r,o){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:r},ih,o)}sendLog(r){return this.notification({method:"notifications/message",params:r})}updateModelContext(r,o){return this._assertInitialized("updateModelContext"),this.request({method:"ui/update-model-context",params:r},Xi,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},nh,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},rh,o)}requestTeardown(r={}){return this.notification({method:"ui/notifications/request-teardown",params:r})}requestDisplayMode(r,o){return this._assertInitialized("requestDisplayMode"),this.request({method:"ui/request-display-mode",params:r},ph,o)}sendSizeChanged(r){return this.notification({method:"ui/notifications/size-changed",params:r})}setupSizeChangedNotifications(){let r=!1,o=0,e=0,i=()=>{r||(r=!0,requestAnimationFrame(()=>{r=!1;let s=document.documentElement,c=s.style.height;s.style.height="max-content";let p=Math.ceil(s.getBoundingClientRect().height);s.style.height=c;let b=Math.ceil(window.innerWidth);(b!==o||p!==e)&&(o=b,e=p,this.sendSizeChanged({width:b,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(r=new Yf(window.parent,window.parent),o){var e;if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1,await super.connect(r);try{let i=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:Gf}},vh,o);if(i===void 0)throw Error(`Server sent invalid initialize result: ${i}`);this._hostCapabilities=i.hostCapabilities,this._hostInfo=i.hostInfo,this._hostContext=i.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,(e=this.options)!=null&&e.autoResize&&this.setupSizeChangedNotifications()}catch(i){throw this.close(),i}}};D(At,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let nr=At;function Vo(t){const n=t&&t.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(t&&t.content||[]).find(o=>o&&o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{}}return{}}class kh{constructor(){D(this,"onhostcontextchanged");D(this,"onerror");D(this,"ontoolresult")}async connect(){var n,r;try{const[o,e]=await Promise.all([this.callServerTool({name:"project_config",arguments:{status:!0}}),this.callServerTool({name:"runtime",arguments:{action:"status"}})]),i=Vo(o),a=Vo(e),s=Array.isArray(i.projects)?i.projects:[],c={projects:s,projects_total:s.length,projects_ready:s.filter(p=>p&&p.ready).length,x_connected:!!i.x_connected,x_state:i.x_state||"",x_handle:i.x_handle??null,version:i.mcp_version||"",latest_version:i.latest_version??null,update_available:!!i.update_available,runtime_ready:typeof a.runtime_ready=="boolean"?a.runtime_ready:!0,runtime_provisioning:!!a.provisioning,onboarding:a.onboarding||i.onboarding};(n=this.ontoolresult)==null||n.call(this,{structuredContent:{snapshot:JSON.stringify(c)}})}catch(o){(r=this.onerror)==null||r.call(this,o)}}getHostContext(){}async callServerTool(n){const r=await fetch(`/tool/${encodeURIComponent(n.name)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n.arguments??{})});if(!r.ok){let o=`HTTP ${r.status}`;try{o=await r.text()||o}catch{}return{isError:!0,content:[{type:"text",text:o}]}}return r.json()}async sendMessage(){return{isError:!0}}}function Sh(){return globalThis.__SAPS_BRIDGE__==="http"?new kh:new nr({name:"S4L Panel",version:"1.0.0"})}function Sl(t){const n=t.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(t.content||[]).find(o=>o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{_raw:r.text}}return{}}const B=t=>document.getElementById(t),wl=B("ver"),Be=B("btn-setup"),Rt=B("btn-schedule"),It=B("stats-grid"),Bn=B("stats-toggle"),wh=B("log"),Ih=B("install-card"),Se=B("setup-summary"),Il=B("onboarding-details"),zh=B("onboarding-steps"),Wn=B("onboarding-blocker"),Bo=B("onboarding-count"),xh=B("onboarding-bar-fill"),jh=B("live-card"),Nh=B("stats-card"),Wo=B("install-steps"),Pe=B("install-err"),he=B("btn-install"),_o=B("btn-live"),bo=B("btn-live-stop"),Ko=B("btn-live-front"),Ue=B("live-status"),Zt=B("live-img"),rr=B("btn-mode"),Th=B("mode-current"),Oh=B("mode-sub");let H=null,Kn=!1,zt=!1,Gn=!1,Ee=!1,Je=!1;function K(t){wh.textContent=t}function Ph(t){switch(t){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function yo(t){if(!t||!Array.isArray(t.steps)){Wo.innerHTML="";return}Wo.innerHTML=t.steps.map(n=>{const r=n.detail&&n.status!=="pending"?` <span class="detail">${n.status==="error"?n.detail:""}</span>`:"";return`<li class="${n.status}"><span class="glyph">${Ph(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),t.error?(Pe.textContent=t.error,Pe.hidden=!1):Pe.hidden=!0}const Uh={environment_checked:"Environment checked",runtime_ready:"Runtime ready",x_connected:"X connected",profile_scanned:"Profile scanned",project_ready:"Project ready",topics_seeded:"Topics seeded",tasks_scheduled:"Tasks scheduled"};function Eh(t){switch(t){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function zl(){Il.hidden=!Ee,Se.setAttribute("aria-expanded",String(Ee)),Se.classList.toggle("expanded",Ee)}function Dh(t){if(!t||!Array.isArray(t.milestones)){Se.hidden=!0,Il.hidden=!0;return}Se.hidden=!1;const n=t.milestones.length,r=t.milestones.filter(e=>e.status==="complete").length,o=!!t.current_blocker&&!t.complete;Se.classList.toggle("complete",t.complete),Se.classList.toggle("blocked",o),Bo.hidden=t.complete,Bo.textContent=o?`${r}/${n} · needs you`:zt?`${r}/${n} · setting up…`:`${r}/${n}`,xh.style.width=n>0?`${Math.round(r/n*100)}%`:"0%",zh.innerHTML=t.milestones.map(e=>{const i=Uh[e.id]||e.id,a=e.attempts>1?` <span class="detail">${e.attempts} attempts</span>`:"";return`<li class="${e.status}"><span class="glyph">${Eh(e.status)}</span><span>${i}${a}</span></li>`}).join(""),t.current_blocker?(Wn.textContent=`Current blocker: ${t.current_blocker.message}`,Wn.hidden=!1,Ee=!0):Wn.hidden=!0,zl()}function Ct(){if(!H)return;Dh(H.onboarding),wl.innerHTML=H.update_available&&H.latest_version?`v${H.version} · <button id="btn-update" class="update-btn">Update to ${H.latest_version}</button>`:`v${H.version}`;const t=!H.runtime_ready;Ih.hidden=!t;const n=H.projects_ready>0,r=!t&&n&&H.x_connected;Be.hidden=r,Be.disabled=!1,Be.classList.toggle("primary",!r);const o=r&&(H.schedule_state==="missing"||H.schedule_state==="disabled");Rt.hidden=!o,Rt.classList.toggle("primary",o);const e=H.mode==="personal_brand";Th.textContent=e?"Personal brand":"Promotion",Oh.textContent=e?"Organic, link-free engagement in your own voice.":"Marketing your configured products.",rr.textContent=e?"Switch to promotion":"Switch to personal brand",jh.hidden=!r,Nh.hidden=!r}function ze(t){H={...H||{},...t},Ct()}function Rh(t){const n=Array.isArray(t.projects)?t.projects:[];return{projects:n,projects_total:n.length,projects_ready:n.filter(r=>r.ready).length,x_connected:!!t.x_connected,x_state:t.x_state||"",x_handle:t.x_handle??null,version:t.mcp_version||(H==null?void 0:H.version)||"",latest_version:t.latest_version??null,update_available:!!t.update_available,mode:t.mode??(H==null?void 0:H.mode),onboarding:t.onboarding}}const be=Sh();function xl(t){var n,r,o;t.theme&&bh(t.theme),(n=t.styles)!=null&&n.variables&&yh(t.styles.variables),(o=(r=t.styles)==null?void 0:r.css)!=null&&o.fonts&&$h(t.styles.css.fonts)}be.onhostcontextchanged=xl;be.onerror=t=>console.error(t);be.ontoolresult=t=>{const n=Sl(t);n&&typeof n.projects_total=="number"&&(ze(n),n.runtime_ready?jl():n.runtime_provisioning&&$o())};async function me(t,n={}){const r=await be.callServerTool({name:t,arguments:n});return Sl(r)}async function Jn(){K("Refreshing…");try{const[t,n]=await Promise.all([me("project_config",{status:!0}),me("runtime",{action:"status"}).catch(()=>({}))]);ze({...Rh(t),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},onboarding:n.onboarding||t.onboarding||(H==null?void 0:H.onboarding)}),H&&!H.runtime_ready&&n.provisioning&&$o(),K(""),jl()}catch(t){K("Refresh failed: "+((t==null?void 0:t.message)||t))}}async function $o(){if(!Kn){Kn=!0,he.disabled=!0,he.textContent="Installing…";try{for(;;){const t=await me("runtime",{action:"status"}).catch(()=>({}));if(yo(t.progress??null),t.onboarding&&ze({onboarding:t.onboarding}),t.runtime_ready){ze({runtime_ready:!0}),K("Runtime installed; you're ready to set up."),Jn();return}const n=t.progress??null;if(n&&n.done&&!n.ok){he.disabled=!1,he.textContent="Retry install",K("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{Kn=!1}}}async function Zh(){var r;if(zt)return;zt=!0,Ct();const t=Date.now(),n=1200*1e3;try{for(;;){const o=await me("runtime",{action:"status"}).catch(()=>({}));o.progress&&yo(o.progress);const e={};if(typeof o.runtime_ready=="boolean"&&(e.runtime_ready=o.runtime_ready),o.onboarding&&(e.onboarding=o.onboarding),Object.keys(e).length&&ze(e),(r=o.onboarding)!=null&&r.complete){await Jn(),K("Setup complete.");break}if(Date.now()-t>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{zt=!1,Ct()}}async function jl(){try{const t=await me("get_stats",{days:7}),n=Array.isArray(t.projects)?t.projects[0]:null,r=n==null?void 0:n.posts;if(!r){It.innerHTML='<div class="muted">No stats yet.</div>';return}const o=[["Posts",r.total??0],["Active",r.active??0],["Views",r.views_period_total??r.views??0],["Replies",r.comments_period_total??r.comments??0],["Clicks",r.post_clicks_period_total??0]];It.innerHTML=o.map(([e,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${e}</div></div>`).join("")}catch(t){It.innerHTML=`<div class="muted">Stats unavailable: ${(t==null?void 0:t.message)||t}</div>`}}function Vn(t,n,r){const o=t.textContent;t.disabled=!0,t.textContent=n,r().finally(()=>{t.disabled=!1,t.textContent=o,Ct()})}Be.addEventListener("click",()=>Vn(Be,"Starting…",async()=>{K("Asking Claude to run setup…");try{const t=await be.sendMessage({role:"user",content:[{type:"text",text:"Set up S4L end to end now. Inspect and repair the runtime, auto-detect and connect my X session, scan my profile, discover and research my product, then infer and save a complete project with seeded search topics. Keep going without asking me to approve each safe setup step. Ask only if I must interactively sign in or no product can be identified. Keep every reply to me extremely concise: a few short sentences at most, no step-by-step narration or long status walls. If you must ask me something (e.g. the product URL), make it one short question."}]});t!=null&&t.isError?K("The host rejected the setup request — type “set up S4L” in the chat instead."):(K("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),Zh())}catch(t){K("Couldn’t start setup: "+((t==null?void 0:t.message)||t))}}));Rt.addEventListener("click",()=>Vn(Rt,"Setting up…",async()=>{K("Asking Claude to schedule the draft tasks for this account…");try{const t=await be.sendMessage({role:"user",content:[{type:"text",text:'Set up the social-autoposter draft autopilot schedule for this Claude account. If queue_setup is available, call it; then for EACH of saps-phase1-query and saps-phase2b-draft call the host tool create_scheduled_task with taskId, cronExpression "* * * * *", and the prompt — read it from ~/.claude/scheduled-tasks/<taskId>/SKILL.md (already on disk). Do NOT redo my X connection or project setup. Keep replies to me very short.'}]});t!=null&&t.isError?K("The host rejected it — type “set up the draft schedule” in the chat instead."):K("Scheduling is running in the chat. The draft tasks will register for this account.")}catch(t){K("Couldn’t start scheduling: "+((t==null?void 0:t.message)||t))}}));rr.addEventListener("click",()=>Vn(rr,"Switching…",async()=>{try{const t=await me("engagement_mode",{action:"toggle"});t&&t.mode&&ze({mode:t.mode}),await Jn()}catch(t){K("Couldn’t switch mode: "+((t==null?void 0:t.message)||t))}}));Se.addEventListener("click",()=>{Ee=!Ee,zl()});Bn.addEventListener("click",()=>{Je=!Je,It.hidden=!Je,Bn.setAttribute("aria-expanded",String(Je)),Bn.classList.toggle("expanded",Je)});wl.addEventListener("click",t=>{const n=t.target;n&&n.id==="btn-update"&&Ch()});async function Ch(){if(Gn)return;Gn=!0;const t=document.getElementById("btn-update");t&&(t.disabled=!0,t.textContent="Updating…"),K("Installing the latest release… this can take a minute.");try{const n=await me("runtime",{action:"update"});n.ok?(K(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),t&&(t.textContent="Update installed — restart to apply")):(K("Update failed (exit "+(n.exit_code??"?")+"). Try `npx social-autoposter@latest update` in a terminal."),t&&(t.disabled=!1,t.textContent="Retry update"))}catch(n){K("Update failed: "+((n==null?void 0:n.message)||n)),t&&(t.disabled=!1,t.textContent="Retry update")}finally{Gn=!1}}he.addEventListener("click",async()=>{Pe.hidden=!0,he.disabled=!0,he.textContent="Starting…",K("Installing the runtime — this is a one-time download (~150MB+).");try{const t=await me("runtime",{action:"install"});if(t.runtime_ready){ze({runtime_ready:!0}),Jn();return}yo(t.progress??null),$o()}catch(t){he.disabled=!1,he.textContent="Retry install",Pe.textContent="Couldn't start install: "+((t==null?void 0:t.message)||t),Pe.hidden=!1}});let xt=null,Qn=!1;async function Go(){if(!Qn){Qn=!0;try{const t=await me("show_browser_to_user",{action:"frame"});if(!t.ok){Ue.textContent=t.message||"No active browser session.",ko(!1);return}t.frame&&(Zt.src=t.frame,Zt.hidden=!1);const n=t.title||t.url||(t.port?"port "+t.port:"");Ue.textContent=t.frame?"Watching"+(n?": "+n:""):"Connecting…"}catch(t){Ue.textContent="Live view error: "+((t==null?void 0:t.message)||t)}finally{Qn=!1}}}function Ah(){_o.hidden=!0,bo.hidden=!1,Ue.textContent="Attaching to the browser…",Go(),xt=setInterval(Go,450)}function ko(t=!0){xt!=null&&(clearInterval(xt),xt=null),_o.hidden=!1,bo.hidden=!0,Zt.hidden=!0,Zt.removeAttribute("src"),t&&me("show_browser_to_user",{action:"stop"}).catch(()=>{})}_o.addEventListener("click",Ah);bo.addEventListener("click",()=>{ko(!0),Ue.textContent=""});Ko.addEventListener("click",()=>Vn(Ko,"Bringing…",async()=>{ko(!0);const t=await me("show_browser_to_user",{action:"front"});Ue.textContent=t!=null&&t.ok?"Brought the browser to the front.":(t==null?void 0:t.message)||"Couldn't bring the browser to the front."}));be.connect().then(()=>{const t=be.getHostContext();t&&xl(t)});</script>
|
|
73
|
+
container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:d().optional().describe("User's language and region preference in BCP 47 format."),timeZone:d().optional().describe("User's timezone in IANA format."),userAgent:d().optional().describe("Host application identifier."),platform:U([u("web"),u("desktop"),u("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:m({touch:M().optional().describe("Whether the device supports touch input."),hover:M().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:m({top:T().describe("Top safe area inset in pixels."),right:T().describe("Right safe area inset in pixels."),bottom:T().describe("Bottom safe area inset in pixels."),left:T().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),gh=m({method:u("ui/notifications/host-context-changed"),params:kl.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:z($t).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:Z(d(),q().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});m({method:u("ui/initialize"),params:m({appInfo:qn.describe("App identification (name and version)."),appCapabilities:mh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var vh=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:qn.describe("Host application identification and version."),hostCapabilities:dh.describe("Features and capabilities provided by the host."),hostContext:kl.describe("Rich context about the host environment.")}).passthrough(),_h={target:"draft-2020-12"};async function Fo(t,n){let r=t["~standard"];if(r.jsonSchema)return r.jsonSchema[n](_h);if(r.vendor==="zod"){let{z:o}=await El(()=>Promise.resolve().then(()=>op),void 0,import.meta.url);return o.toJSONSchema(t,{io:n})}throw Error(`Schema (vendor: ${r.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function Jo(t,n,r=""){let o=await t["~standard"].validate(n);if(o.issues){let e=o.issues.map(i=>{var s;let a=(s=i.path)==null?void 0:s.map(c=>typeof c=="object"?c.key:c).join(".");return a?`${a}: ${i.message}`:i.message}).join("; ");throw Error(r+e)}return o.value}function bh(t){let n=document.documentElement;n.setAttribute("data-theme",t),n.style.colorScheme=t}function yh(t,n=document.documentElement){for(let[r,o]of Object.entries(t))o!==void 0&&n.style.setProperty(r,o)}function $h(t){if(document.getElementById("__mcp-host-fonts"))return;let n=document.createElement("style");n.id="__mcp-host-fonts",n.textContent=t,document.head.appendChild(n)}const At=class At extends Kf{constructor(r,o={},e={autoResize:!0}){super(e);D(this,"_appInfo");D(this,"_capabilities");D(this,"options");D(this,"_hostCapabilities");D(this,"_hostInfo");D(this,"_hostContext");D(this,"_registeredTools",{});D(this,"_initializedSent",!1);D(this,"eventSchemas",{toolinput:oh,toolinputpartial:ah,toolresult:hh,toolcancelled:sh,hostcontextchanged:gh});D(this,"_everHadListener",new Set);D(this,"_toolHandlersInitialized",!1);D(this,"_onteardown");D(this,"_oncalltool");D(this,"_onlisttools");D(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=e,e.allowUnsafeEval||X({jitless:!0}),this.setRequestHandler(Hn,i=>(console.log("Received ping:",i.params),{})),this.setEventHandler("hostcontextchanged",void 0)}_assertInitialized(r){var e;if(this._initializedSent)return;let o=`[ext-apps] App.${r}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if((e=this.options)!=null&&e.strict)throw Error(o);console.warn(`${o}. This will throw in a future release.`)}_assertHandlerTiming(r){var e;if(!At.ONE_SHOT_EVENTS.has(r)||this._everHadListener.has(r)||(this._everHadListener.add(r),!this._initializedSent))return;let o=`[ext-apps] "${String(r)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if((e=this.options)!=null&&e.strict)throw Error(o);console.warn(o)}setEventHandler(r,o){o&&this._assertHandlerTiming(r),super.setEventHandler(r,o)}addEventListener(r,o){this._assertHandlerTiming(r),super.addEventListener(r,o)}onEventDispatch(r,o){r==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...o})}registerCapabilities(r){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._capabilities=Wf(this._capabilities,r)}registerTool(r,o,e){if(this._registeredTools[r])throw Error(`Tool ${r} is already registered`);let i=this,a=()=>{var p;i._initializedSent&&((p=i._capabilities.tools)!=null&&p.listChanged)&&i.sendToolListChanged()},s=o.inputSchema!==void 0,c={title:o.title,description:o.description,inputSchema:o.inputSchema,outputSchema:o.outputSchema,annotations:o.annotations,_meta:o._meta,enabled:!0,enable(){this.enabled=!0,a()},disable(){this.enabled=!1,a()},update(p){Object.assign(this,p),a()},remove(){i._registeredTools[r]===c&&(delete i._registeredTools[r],a())},handler:async(p,b)=>{if(!c.enabled)throw Error(`Tool ${r} is disabled`);let g;if(s){let $=c.inputSchema,w=$?await Jo($,p??{},`Invalid input for tool ${r}: `):p??{};g=await e(w,b)}else g=await e(b);return c.outputSchema&&!g.isError&&(g.structuredContent=await Jo(c.outputSchema,g.structuredContent,`Invalid output for tool ${r}: `)),g}};return this._registeredTools[r]=c,!this._capabilities.tools&&!this.transport&&this.registerCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),a(),c}ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(r,o)=>{let e=this._registeredTools[r.name];if(!e)throw Error(`Tool ${r.name} not found`);return e.handler(r.arguments,o)},this.onlisttools=async(r,o)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([e,i])=>i.enabled).map(async([e,i])=>{let a={name:e,title:i.title,description:i.description,inputSchema:i.inputSchema?await Fo(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await Fo(i.outputSchema,"output")),i.annotations&&(a.annotations=i.annotations),i._meta&&(a._meta=i._meta),a}))}))}async sendToolListChanged(r={}){this._assertInitialized("sendToolListChanged"),await this.notification({method:"notifications/tools/list_changed",params:r})}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}get ontoolinput(){return this.getEventHandler("toolinput")}set ontoolinput(r){this.setEventHandler("toolinput",r)}get ontoolinputpartial(){return this.getEventHandler("toolinputpartial")}set ontoolinputpartial(r){this.setEventHandler("toolinputpartial",r)}get ontoolresult(){return this.getEventHandler("toolresult")}set ontoolresult(r){this.setEventHandler("toolresult",r)}get ontoolcancelled(){return this.getEventHandler("toolcancelled")}set ontoolcancelled(r){this.setEventHandler("toolcancelled",r)}get onhostcontextchanged(){return this.getEventHandler("hostcontextchanged")}set onhostcontextchanged(r){this.setEventHandler("hostcontextchanged",r)}get onteardown(){return this._onteardown}set onteardown(r){this.warnIfRequestHandlerReplaced("onteardown",this._onteardown,r),this._onteardown=r,this.replaceRequestHandler(lh,(o,e)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(o.params,e)})}get oncalltool(){return this._oncalltool}set oncalltool(r){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,r),this._oncalltool=r,this.replaceRequestHandler(_l,(o,e)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(o.params,e)})}get onlisttools(){return this._onlisttools}set onlisttools(r){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,r),this._onlisttools=r,this.replaceRequestHandler(vl,(o,e)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(o.params,e)})}assertCapabilityForMethod(r){var o;switch(r){case"sampling/createMessage":if(!((o=this._hostCapabilities)!=null&&o.sampling))throw Error(`Host does not support sampling (required for ${r})`);break}}assertRequestHandlerCapability(r){switch(r){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${r})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${r} registered`)}}assertNotificationCapability(r){}assertTaskCapability(r){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(r){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(r,o){if(this._assertInitialized("callServerTool"),typeof r=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${r}"). Did you mean: callServerTool({ name: "${r}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:r},Fn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},fl,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},pl,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let e=r.tools?$l:yl;return await this.request({method:"sampling/createMessage",params:r},e,o)}sendMessage(r,o){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:r},ih,o)}sendLog(r){return this.notification({method:"notifications/message",params:r})}updateModelContext(r,o){return this._assertInitialized("updateModelContext"),this.request({method:"ui/update-model-context",params:r},Xi,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},nh,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},rh,o)}requestTeardown(r={}){return this.notification({method:"ui/notifications/request-teardown",params:r})}requestDisplayMode(r,o){return this._assertInitialized("requestDisplayMode"),this.request({method:"ui/request-display-mode",params:r},ph,o)}sendSizeChanged(r){return this.notification({method:"ui/notifications/size-changed",params:r})}setupSizeChangedNotifications(){let r=!1,o=0,e=0,i=()=>{r||(r=!0,requestAnimationFrame(()=>{r=!1;let s=document.documentElement,c=s.style.height;s.style.height="max-content";let p=Math.ceil(s.getBoundingClientRect().height);s.style.height=c;let b=Math.ceil(window.innerWidth);(b!==o||p!==e)&&(o=b,e=p,this.sendSizeChanged({width:b,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(r=new Yf(window.parent,window.parent),o){var e;if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1,await super.connect(r);try{let i=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:Gf}},vh,o);if(i===void 0)throw Error(`Server sent invalid initialize result: ${i}`);this._hostCapabilities=i.hostCapabilities,this._hostInfo=i.hostInfo,this._hostContext=i.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,(e=this.options)!=null&&e.autoResize&&this.setupSizeChangedNotifications()}catch(i){throw this.close(),i}}};D(At,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let nr=At;function Vo(t){const n=t&&t.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(t&&t.content||[]).find(o=>o&&o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{}}return{}}class kh{constructor(){D(this,"onhostcontextchanged");D(this,"onerror");D(this,"ontoolresult")}async connect(){var n,r;try{const[o,e]=await Promise.all([this.callServerTool({name:"project_config",arguments:{status:!0}}),this.callServerTool({name:"runtime",arguments:{action:"status"}})]),i=Vo(o),a=Vo(e),s=Array.isArray(i.projects)?i.projects:[],c={projects:s,projects_total:s.length,projects_ready:s.filter(p=>p&&p.ready).length,x_connected:!!i.x_connected,x_state:i.x_state||"",x_handle:i.x_handle??null,version:i.mcp_version||"",latest_version:i.latest_version??null,update_available:!!i.update_available,runtime_ready:typeof a.runtime_ready=="boolean"?a.runtime_ready:!0,runtime_provisioning:!!a.provisioning,onboarding:a.onboarding||i.onboarding};(n=this.ontoolresult)==null||n.call(this,{structuredContent:{snapshot:JSON.stringify(c)}})}catch(o){(r=this.onerror)==null||r.call(this,o)}}getHostContext(){}async callServerTool(n){const r=await fetch(`/tool/${encodeURIComponent(n.name)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n.arguments??{})});if(!r.ok){let o=`HTTP ${r.status}`;try{o=await r.text()||o}catch{}return{isError:!0,content:[{type:"text",text:o}]}}return r.json()}async sendMessage(){return{isError:!0}}}function Sh(){return globalThis.__SAPS_BRIDGE__==="http"?new kh:new nr({name:"S4L Panel",version:"1.0.0"})}function Sl(t){const n=t.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(t.content||[]).find(o=>o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{_raw:r.text}}return{}}const B=t=>document.getElementById(t),wl=B("ver"),Be=B("btn-setup"),Rt=B("btn-schedule"),It=B("stats-grid"),Bn=B("stats-toggle"),wh=B("log"),Ih=B("install-card"),Se=B("setup-summary"),Il=B("onboarding-details"),zh=B("onboarding-steps"),Wn=B("onboarding-blocker"),Bo=B("onboarding-count"),xh=B("onboarding-bar-fill"),jh=B("live-card"),Nh=B("stats-card"),Wo=B("install-steps"),Pe=B("install-err"),he=B("btn-install"),_o=B("btn-live"),bo=B("btn-live-stop"),Ko=B("btn-live-front"),Ue=B("live-status"),Zt=B("live-img"),rr=B("btn-mode"),Th=B("mode-current"),Oh=B("mode-sub");let H=null,Kn=!1,zt=!1,Gn=!1,Ee=!1,Je=!1;function K(t){wh.textContent=t}function Ph(t){switch(t){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function yo(t){if(!t||!Array.isArray(t.steps)){Wo.innerHTML="";return}Wo.innerHTML=t.steps.map(n=>{const r=n.detail&&n.status!=="pending"?` <span class="detail">${n.status==="error"?n.detail:""}</span>`:"";return`<li class="${n.status}"><span class="glyph">${Ph(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),t.error?(Pe.textContent=t.error,Pe.hidden=!1):Pe.hidden=!0}const Uh={environment_checked:"Environment checked",runtime_ready:"Runtime ready",x_connected:"X connected",profile_scanned:"Profile scanned",project_ready:"Project ready",topics_seeded:"Topics seeded",tasks_scheduled:"Tasks scheduled"};function Eh(t){switch(t){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function zl(){Il.hidden=!Ee,Se.setAttribute("aria-expanded",String(Ee)),Se.classList.toggle("expanded",Ee)}function Dh(t){if(!t||!Array.isArray(t.milestones)){Se.hidden=!0,Il.hidden=!0;return}Se.hidden=!1;const n=t.milestones.length,r=t.milestones.filter(e=>e.status==="complete").length,o=!!t.current_blocker&&!t.complete;Se.classList.toggle("complete",t.complete),Se.classList.toggle("blocked",o),Bo.hidden=t.complete,Bo.textContent=o?`${r}/${n} · needs you`:zt?`${r}/${n} · setting up…`:`${r}/${n}`,xh.style.width=n>0?`${Math.round(r/n*100)}%`:"0%",zh.innerHTML=t.milestones.map(e=>{const i=Uh[e.id]||e.id,a=e.attempts>1?` <span class="detail">${e.attempts} attempts</span>`:"";return`<li class="${e.status}"><span class="glyph">${Eh(e.status)}</span><span>${i}${a}</span></li>`}).join(""),t.current_blocker?(Wn.textContent=`Current blocker: ${t.current_blocker.message}`,Wn.hidden=!1,Ee=!0):Wn.hidden=!0,zl()}function Ct(){if(!H)return;Dh(H.onboarding),wl.innerHTML=H.update_available&&H.latest_version?`v${H.version} · <button id="btn-update" class="update-btn">Update to ${H.latest_version}</button>`:`v${H.version}`;const t=!H.runtime_ready;Ih.hidden=!t;const n=H.projects_ready>0,r=!t&&n&&H.x_connected;Be.hidden=r,Be.disabled=!1,Be.classList.toggle("primary",!r);const o=r&&(H.schedule_state==="missing"||H.schedule_state==="disabled");Rt.hidden=!o,Rt.classList.toggle("primary",o);const e=H.mode==="personal_brand";Th.textContent=e?"Personal brand":"Promotion",Oh.textContent=e?"Organic, link-free engagement in your own voice.":"Marketing your configured products.",rr.textContent=e?"Switch to promotion":"Switch to personal brand",jh.hidden=!r,Nh.hidden=!r}function ze(t){H={...H||{},...t},Ct()}function Rh(t){const n=Array.isArray(t.projects)?t.projects:[];return{projects:n,projects_total:n.length,projects_ready:n.filter(r=>r.ready).length,x_connected:!!t.x_connected,x_state:t.x_state||"",x_handle:t.x_handle??null,version:t.mcp_version||(H==null?void 0:H.version)||"",latest_version:t.latest_version??null,update_available:!!t.update_available,mode:t.mode??(H==null?void 0:H.mode),onboarding:t.onboarding}}const be=Sh();function xl(t){var n,r,o;t.theme&&bh(t.theme),(n=t.styles)!=null&&n.variables&&yh(t.styles.variables),(o=(r=t.styles)==null?void 0:r.css)!=null&&o.fonts&&$h(t.styles.css.fonts)}be.onhostcontextchanged=xl;be.onerror=t=>console.error(t);be.ontoolresult=t=>{const n=Sl(t);n&&typeof n.projects_total=="number"&&(ze(n),n.runtime_ready?jl():n.runtime_provisioning&&$o())};async function me(t,n={}){const r=await be.callServerTool({name:t,arguments:n});return Sl(r)}async function Jn(){K("Refreshing…");try{const[t,n]=await Promise.all([me("project_config",{status:!0}),me("runtime",{action:"status"}).catch(()=>({}))]);ze({...Rh(t),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},onboarding:n.onboarding||t.onboarding||(H==null?void 0:H.onboarding)}),H&&!H.runtime_ready&&n.provisioning&&$o(),K(""),jl()}catch(t){K("Refresh failed: "+((t==null?void 0:t.message)||t))}}async function $o(){if(!Kn){Kn=!0,he.disabled=!0,he.textContent="Installing…";try{for(;;){const t=await me("runtime",{action:"status"}).catch(()=>({}));if(yo(t.progress??null),t.onboarding&&ze({onboarding:t.onboarding}),t.runtime_ready){ze({runtime_ready:!0}),K("Runtime installed; you're ready to set up."),Jn();return}const n=t.progress??null;if(n&&n.done&&!n.ok){he.disabled=!1,he.textContent="Retry install",K("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{Kn=!1}}}async function Zh(){var r;if(zt)return;zt=!0,Ct();const t=Date.now(),n=1200*1e3;try{for(;;){const o=await me("runtime",{action:"status"}).catch(()=>({}));o.progress&&yo(o.progress);const e={};if(typeof o.runtime_ready=="boolean"&&(e.runtime_ready=o.runtime_ready),o.onboarding&&(e.onboarding=o.onboarding),Object.keys(e).length&&ze(e),(r=o.onboarding)!=null&&r.complete){await Jn(),K("Setup complete.");break}if(Date.now()-t>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{zt=!1,Ct()}}async function jl(){try{const t=await me("get_stats",{days:7}),n=Array.isArray(t.projects)?t.projects[0]:null,r=n==null?void 0:n.posts;if(!r){It.innerHTML='<div class="muted">No stats yet.</div>';return}const o=[["Posts",r.total??0],["Active",r.active??0],["Views",r.views_period_total??r.views??0],["Replies",r.comments_period_total??r.comments??0],["Clicks",r.post_clicks_period_total??0]];It.innerHTML=o.map(([e,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${e}</div></div>`).join("")}catch(t){It.innerHTML=`<div class="muted">Stats unavailable: ${(t==null?void 0:t.message)||t}</div>`}}function Vn(t,n,r){const o=t.textContent;t.disabled=!0,t.textContent=n,r().finally(()=>{t.disabled=!1,t.textContent=o,Ct()})}Be.addEventListener("click",()=>Vn(Be,"Starting…",async()=>{K("Asking Claude to run setup…");try{const t=await be.sendMessage({role:"user",content:[{type:"text",text:"Set up S4L plugin end to end now. Inspect and repair the runtime, auto-detect and connect my X session, scan my profile, discover and research my product, then infer and save a complete project with seeded search topics. Keep going without asking me to approve each safe setup step. Ask only if I must interactively sign in or no product can be identified. Keep every reply to me extremely concise: a few short sentences at most, no step-by-step narration or long status walls. If you must ask me something (e.g. the product URL), make it one short question."}]});t!=null&&t.isError?K("The host rejected the setup request — type “set up S4L” in the chat instead."):(K("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),Zh())}catch(t){K("Couldn’t start setup: "+((t==null?void 0:t.message)||t))}}));Rt.addEventListener("click",()=>Vn(Rt,"Setting up…",async()=>{K("Asking Claude to schedule the draft tasks for this account…");try{const t=await be.sendMessage({role:"user",content:[{type:"text",text:'Set up the social-autoposter draft autopilot schedule for this Claude account. If queue_setup is available, call it; then for EACH of saps-phase1-query and saps-phase2b-draft call the host tool create_scheduled_task with taskId, cronExpression "* * * * *", and the prompt — read it from ~/.claude/scheduled-tasks/<taskId>/SKILL.md (already on disk). Do NOT redo my X connection or project setup. Keep replies to me very short.'}]});t!=null&&t.isError?K("The host rejected it — type “set up the draft schedule” in the chat instead."):K("Scheduling is running in the chat. The draft tasks will register for this account.")}catch(t){K("Couldn’t start scheduling: "+((t==null?void 0:t.message)||t))}}));rr.addEventListener("click",()=>Vn(rr,"Switching…",async()=>{try{const t=await me("engagement_mode",{action:"toggle"});t&&t.mode&&ze({mode:t.mode}),await Jn()}catch(t){K("Couldn’t switch mode: "+((t==null?void 0:t.message)||t))}}));Se.addEventListener("click",()=>{Ee=!Ee,zl()});Bn.addEventListener("click",()=>{Je=!Je,It.hidden=!Je,Bn.setAttribute("aria-expanded",String(Je)),Bn.classList.toggle("expanded",Je)});wl.addEventListener("click",t=>{const n=t.target;n&&n.id==="btn-update"&&Ch()});async function Ch(){if(Gn)return;Gn=!0;const t=document.getElementById("btn-update");t&&(t.disabled=!0,t.textContent="Updating…"),K("Installing the latest release… this can take a minute.");try{const n=await me("runtime",{action:"update"});n.ok?(K(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),t&&(t.textContent="Update installed — restart to apply")):(K("Update failed (exit "+(n.exit_code??"?")+"). Try `npx social-autoposter@latest update` in a terminal."),t&&(t.disabled=!1,t.textContent="Retry update"))}catch(n){K("Update failed: "+((n==null?void 0:n.message)||n)),t&&(t.disabled=!1,t.textContent="Retry update")}finally{Gn=!1}}he.addEventListener("click",async()=>{Pe.hidden=!0,he.disabled=!0,he.textContent="Starting…",K("Installing the runtime — this is a one-time download (~150MB+).");try{const t=await me("runtime",{action:"install"});if(t.runtime_ready){ze({runtime_ready:!0}),Jn();return}yo(t.progress??null),$o()}catch(t){he.disabled=!1,he.textContent="Retry install",Pe.textContent="Couldn't start install: "+((t==null?void 0:t.message)||t),Pe.hidden=!1}});let xt=null,Qn=!1;async function Go(){if(!Qn){Qn=!0;try{const t=await me("show_browser_to_user",{action:"frame"});if(!t.ok){Ue.textContent=t.message||"No active browser session.",ko(!1);return}t.frame&&(Zt.src=t.frame,Zt.hidden=!1);const n=t.title||t.url||(t.port?"port "+t.port:"");Ue.textContent=t.frame?"Watching"+(n?": "+n:""):"Connecting…"}catch(t){Ue.textContent="Live view error: "+((t==null?void 0:t.message)||t)}finally{Qn=!1}}}function Ah(){_o.hidden=!0,bo.hidden=!1,Ue.textContent="Attaching to the browser…",Go(),xt=setInterval(Go,450)}function ko(t=!0){xt!=null&&(clearInterval(xt),xt=null),_o.hidden=!1,bo.hidden=!0,Zt.hidden=!0,Zt.removeAttribute("src"),t&&me("show_browser_to_user",{action:"stop"}).catch(()=>{})}_o.addEventListener("click",Ah);bo.addEventListener("click",()=>{ko(!0),Ue.textContent=""});Ko.addEventListener("click",()=>Vn(Ko,"Bringing…",async()=>{ko(!0);const t=await me("show_browser_to_user",{action:"front"});Ue.textContent=t!=null&&t.ok?"Brought the browser to the front.":(t==null?void 0:t.message)||"Couldn't bring the browser to the front."}));be.connect().then(()=>{const t=be.getHostContext();t&&xl(t)});</script>
|
|
74
74
|
<style rel="stylesheet" crossorigin>:root{--bg: var(--background, #ffffff);--fg: var(--foreground, #111111);--muted: var(--muted-foreground, #6b6b6b);--card: var(--card, #f5f5f5);--border: var(--border, #e2e2e2);--btn-fg: var(--primary-foreground, #ffffff);--btn-bg: var(--primary, #111111);--btn-border: var(--border, #c4c4c4)}@media(prefers-color-scheme:dark){:root{--bg: var(--background, #1a1a1a);--fg: var(--foreground, #f2f2f2);--muted: var(--muted-foreground, #9a9a9a);--card: var(--card, #262626);--border: var(--border, #3a3a3a);--btn-fg: var(--primary-foreground, #111111);--btn-bg: var(--primary, #f2f2f2);--btn-border: var(--border, #555555)}}*{box-sizing:border-box}html,body{margin:0;padding:0;background:var(--bg);color:var(--fg);font-family:var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);font-size:14px;line-height:1.4}.wrap{max-width:640px;margin:0 auto;padding:14px;display:flex;flex-direction:column;gap:10px}.head{display:flex;align-items:center;justify-content:space-between;gap:8px}.head-left{display:inline-flex;align-items:baseline;gap:8px;min-width:0}.title{font-size:16px;font-weight:650;letter-spacing:-.01em}.ver{font-size:12px;color:var(--muted);display:inline-flex;align-items:center;gap:6px}.ver button.update-btn{padding:3px 10px;font-size:12px;font-weight:600;border-radius:7px;background:var(--btn-bg);color:var(--btn-fg);border:1px solid var(--btn-bg)}.setup-summary{display:inline-flex;align-items:center;gap:6px;flex:none;padding:5px 10px;font-size:12px;font-weight:550;color:var(--muted);background:var(--bg);border:1px solid var(--border);border-radius:8px}.setup-summary[hidden]{display:none}.setup-summary.complete{color:var(--fg)}.setup-summary-label{font-weight:600}.setup-summary-count{color:var(--muted)}.setup-summary-count[hidden]{display:none}.setup-summary.blocked .setup-summary-count{color:var(--fg);font-weight:600}.chevron{width:0;height:0;flex:none;border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid currentColor;transition:transform .15s ease}.expanded .chevron{transform:rotate(180deg)}button{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:1px solid var(--btn-border);background:var(--card);color:var(--fg);border-radius:8px;padding:7px 12px;font-size:13px;font-weight:550;line-height:1.2;white-space:nowrap;cursor:pointer;transition:opacity .12s ease}button:hover:not(:disabled){opacity:.82}button:disabled{opacity:.45;cursor:default}button.primary{background:var(--btn-bg);color:var(--btn-fg);border-color:var(--btn-bg)}button.ghost{background:var(--bg)}button[hidden]{display:none}.actions{display:flex;flex-wrap:wrap;gap:8px}.section{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:8px}.section[hidden]{display:none}.section-row{display:flex;align-items:center;justify-content:space-between;gap:8px 12px;width:100%;flex-wrap:wrap}.section-label{font-size:13px;font-weight:600;color:var(--fg)}.section-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}button.section-row-toggle{background:none;border:none;padding:0;border-radius:0;font:inherit;color:inherit}button.section-row-toggle:hover:not(:disabled){opacity:1}.section-row-toggle .chevron{color:var(--muted)}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(88px,1fr));gap:8px}.stats-grid[hidden]{display:none}.stat{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:8px 10px}.stat .n{font-size:17px;font-weight:650}.stat .l{font-size:11px;color:var(--muted);margin-top:1px}.config-desc{font-size:12px;color:var(--muted)}.config-desc[hidden]{display:none}.config-editor{width:100%;min-height:260px;resize:vertical;background:var(--bg);color:var(--fg);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-family:var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);font-size:12px;line-height:1.45;-moz-tab-size:2;tab-size:2;white-space:pre;overflow:auto}.config-editor[hidden]{display:none}.config-status{font-size:11px;color:var(--muted);white-space:pre-wrap;word-break:break-all}.live-status{font-size:11px;color:var(--muted);min-height:14px}.live-img{display:block;width:100%;border:1px solid var(--border);border-radius:6px;background:#000}.live-img[hidden]{display:none}.install{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:12px;display:flex;flex-direction:column;gap:10px}.install[hidden]{display:none}.install-head{font-size:14px;font-weight:650}.install-desc{font-size:12px;color:var(--muted)}.install-actions{display:flex;gap:8px}.install-err{font-size:12px;color:var(--fg);font-weight:600;white-space:pre-wrap}.install-err[hidden]{display:none}.steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}.steps li{display:flex;align-items:baseline;gap:8px;font-size:13px;color:var(--muted)}.steps li .glyph{width:1.1em;flex:none;text-align:center;font-weight:650;color:var(--fg)}.steps li.running,.steps li.done,.steps li.complete,.steps li.in_progress{color:var(--fg)}.steps li.pending{opacity:.55}.steps li.error,.steps li.blocked{color:var(--fg);font-weight:600}.steps li .detail{font-size:11px;color:var(--muted)}.onboarding-details{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:12px;display:flex;flex-direction:column;gap:10px}.onboarding-details[hidden]{display:none}.onboarding-bar{height:4px;border-radius:999px;background:var(--border);overflow:hidden}.onboarding-bar>span{display:block;height:100%;width:0%;background:var(--fg);border-radius:inherit;transition:width .35s ease}.onboarding-steps{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px 12px}.onboarding-blocker{border-top:1px solid var(--border);padding-top:9px;color:var(--fg);font-size:12px;font-weight:600;white-space:pre-wrap}.onboarding-blocker[hidden]{display:none}@media(max-width:520px){.onboarding-steps{grid-template-columns:1fr}}.log{font-size:12px;color:var(--muted);min-height:16px;white-space:pre-wrap}.muted{color:var(--muted)}</style>
|
|
75
75
|
</head>
|
|
76
76
|
<body>
|
package/mcp/dist/version.json
CHANGED
package/mcp/manifest.json
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"dxt_version": "0.1",
|
|
3
3
|
"name": "social-autoposter",
|
|
4
4
|
"display_name": "S4L",
|
|
5
|
-
"version": "1.6.
|
|
5
|
+
"version": "1.6.166",
|
|
6
6
|
"description": "Draft, review, approve, and autopilot X/Twitter posts.",
|
|
7
|
-
"long_description": "The disclaimer above is generic Claude boilerplate. S4L 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 end to end**\n\n2\\. Quit fully with CMD+Q, restart Claude, and paste the prompt into a new chat.",
|
|
7
|
+
"long_description": "The disclaimer above is generic Claude boilerplate. S4L 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 fully with CMD+Q, restart Claude, and paste the prompt into a new chat.",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "S4L.ai",
|
|
10
10
|
"email": "i@m13v.com",
|
|
@@ -104,7 +104,7 @@ SPINNER = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
|
104
104
|
# SETUP_PROMPT mirrors the in-chat panel's Setup button (panel.ts) verbatim so
|
|
105
105
|
# both entry points kick off the same end-to-end flow.
|
|
106
106
|
SETUP_PROMPT = (
|
|
107
|
-
"Set up social autoposter end to end now. Inspect and repair the runtime, "
|
|
107
|
+
"Set up social autoposter plugin end to end now. Inspect and repair the runtime, "
|
|
108
108
|
"auto-detect and connect my X session, scan my profile, discover and research "
|
|
109
109
|
"my product, then infer and save a complete project with seeded search topics. "
|
|
110
110
|
"Keep going without asking me to approve each safe setup step. Ask only if I "
|
package/mcp/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m13v/social-autoposter-mcp",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.166",
|
|
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
|
@@ -36,6 +36,7 @@ down it sleeps and retries; it never crashes the pipeline.
|
|
|
36
36
|
|
|
37
37
|
from __future__ import annotations
|
|
38
38
|
|
|
39
|
+
import fcntl
|
|
39
40
|
import glob
|
|
40
41
|
import json
|
|
41
42
|
import os
|
|
@@ -454,6 +455,20 @@ def cmd_watch(interval: float = 2.0) -> int:
|
|
|
454
455
|
holds ONE CDP connection open across ticks (light, and friendly to the
|
|
455
456
|
poster's concurrent CDP session) and only reconnects when the harness Chrome
|
|
456
457
|
comes/goes. Never raises into the pipeline."""
|
|
458
|
+
# Singleton guard: there must be exactly ONE watcher painting at a time, or
|
|
459
|
+
# two loops fight over the same overlay (double heartbeat, flicker). Two start
|
|
460
|
+
# lanes can race to spawn this: the MCP's foreground KeepAlive launchd job and
|
|
461
|
+
# the best-effort run-overlay-watch.sh supervisor. Hold an exclusive,
|
|
462
|
+
# non-blocking flock for the life of the process; if another watcher already
|
|
463
|
+
# holds it, exit 0 quietly and let that one own the overlay. The lock fd is
|
|
464
|
+
# intentionally leaked (kept open) until the process dies so the OS releases
|
|
465
|
+
# it automatically on exit/kill.
|
|
466
|
+
try:
|
|
467
|
+
_lock_fd = os.open("/tmp/saps_overlay_watch.lock", os.O_CREAT | os.O_RDWR, 0o644)
|
|
468
|
+
fcntl.flock(_lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
469
|
+
except OSError:
|
|
470
|
+
print("another overlay watcher already running; exiting", file=sys.stderr)
|
|
471
|
+
return 0
|
|
457
472
|
print(f"watching {LOG_DIR}/twitter-cycle-*.log -> overlay on {CDP_URL} (Ctrl-C to stop)")
|
|
458
473
|
# Treat SIGTERM (launchd unload, `kill`) like Ctrl-C so the overlay is
|
|
459
474
|
# cleared on the way out instead of lingering until the next navigation.
|
package/setup/SKILL.md
CHANGED
|
@@ -13,7 +13,7 @@ goal, not as the beginning of an interview.
|
|
|
13
13
|
- Keep taking the next safe setup action until the system is working end to end.
|
|
14
14
|
- Do not ask whether to run setup, install a dependency, inspect status, connect
|
|
15
15
|
X, scan the profile, research the website, save inferred fields, seed topics,
|
|
16
|
-
retry a recoverable failure
|
|
16
|
+
or retry a recoverable failure. Do them.
|
|
17
17
|
- An explicit request to set up social-autoposter authorizes its owned local
|
|
18
18
|
runtime installation and importing only x.com/twitter.com session cookies
|
|
19
19
|
into its managed browser. Briefly warn that macOS keychain prompts may appear,
|
|
@@ -26,8 +26,13 @@ goal, not as the beginning of an interview.
|
|
|
26
26
|
there is no configured project, no clear product URL in context or the X
|
|
27
27
|
profile, and no way to identify what product to market.
|
|
28
28
|
- Never post a draft during setup unless the user explicitly asked for that.
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
- Never hand-run the X cycle. Do not run `run-twitter-cycle.sh`,
|
|
30
|
+
`claude_job.py`, or any cycle script directly, and never offer to "trigger a
|
|
31
|
+
cycle" or "run one now" so a draft appears faster. Only the launchd kicker may
|
|
32
|
+
fire the cycle, with its required environment. A manual kick produces an
|
|
33
|
+
empty-plan artifact and blocks the autopilot. Verification means scheduling
|
|
34
|
+
the autopilot and letting the kicker drive it; you wait and poll, you never
|
|
35
|
+
trigger.
|
|
31
36
|
- Do not edit the MCP server, plugin source, or an unrelated user workspace to
|
|
32
37
|
work around setup failures. Use the product's setup/install tools.
|
|
33
38
|
|
|
@@ -156,10 +161,14 @@ readiness. Optional/recommended fields never justify stopping setup.
|
|
|
156
161
|
|
|
157
162
|
Schedule the draft autopilot: call `queue_setup`, then for EACH returned task
|
|
158
163
|
call the host tool `create_scheduled_task` (taskId, cronExpression, prompt
|
|
159
|
-
verbatim; "already exists" is fine). The autopilot then drafts on its own —
|
|
164
|
+
verbatim; "already exists" is fine). The autopilot then drafts on its own — the
|
|
160
165
|
launchd kicker fires a draft-only cycle and the queue worker drafts the replies;
|
|
161
|
-
nothing posts.
|
|
162
|
-
returned review batch is the strongest success signal.
|
|
166
|
+
nothing posts. Then wait: poll the `dashboard` tool until the pending-draft
|
|
167
|
+
count rises; a returned review batch is the strongest success signal.
|
|
168
|
+
|
|
169
|
+
Do not hand-run a cycle to make this happen faster, and do not offer the user to
|
|
170
|
+
trigger one. Scheduling the autopilot is the only sanctioned way to produce the
|
|
171
|
+
verification draft; the kicker drives it on its own minute-by-minute schedule.
|
|
163
172
|
|
|
164
173
|
If no card appears, diagnose the fixable reason, fix it, and let the autopilot
|
|
165
174
|
retry on its next scheduled cycle:
|
|
@@ -195,9 +204,12 @@ turning it into instructions for the user.
|
|
|
195
204
|
`python3 ~/social-autoposter/scripts/setup_twitter_auth.py connect`
|
|
196
205
|
The user may need to approve a macOS keychain prompt or sign in once in the
|
|
197
206
|
managed browser; continue automatically afterward.
|
|
198
|
-
7.
|
|
199
|
-
|
|
200
|
-
|
|
207
|
+
7. Do not load launchd/autopilot jobs unless explicitly requested, and never
|
|
208
|
+
hand-run `run-twitter-cycle.sh` or any cycle script to "verify." A manual
|
|
209
|
+
kick produces an empty-plan artifact and blocks the autopilot. The cycle runs
|
|
210
|
+
only when the launchd autopilot is scheduled and the kicker fires it. In CLI
|
|
211
|
+
fallback, setup is configured once the project is saved, topics are seeded,
|
|
212
|
+
and X is connected; the first draft appears after the autopilot is scheduled.
|
|
201
213
|
|
|
202
214
|
## Completion summary
|
|
203
215
|
|
|
@@ -73,6 +73,20 @@ WATCH_LOG="${SAPS_LOG_DIR}/overlay-watch.log"
|
|
|
73
73
|
# --- spawn detached ----------------------------------------------------------
|
|
74
74
|
cd "${REPO_DIR}" || exit 0
|
|
75
75
|
echo "[overlay-watch] $(date '+%Y-%m-%d %H:%M:%S') starting watcher py=${PYBIN} cdp=${TWITTER_CDP_URL} log=${SAPS_LOG_DIR}" >>"${WATCH_LOG}" 2>&1
|
|
76
|
-
|
|
76
|
+
# Spawn the watcher in a NEW SESSION so it outlives this supervisor.
|
|
77
|
+
# When launchd fires this script (StartInterval 60), the script is the job's
|
|
78
|
+
# main process; the moment it exits, launchd reaps the WHOLE job process group.
|
|
79
|
+
# `nohup` only blocks SIGHUP, not that group SIGKILL, so a plain `nohup ... &`
|
|
80
|
+
# child dies the instant we `exit 0` (it survives only when this runs from an
|
|
81
|
+
# interactive shell, which is NOT a launchd job). macOS ships no setsid(1), so
|
|
82
|
+
# use the python we already resolved to os.setsid() off the launchd group, then
|
|
83
|
+
# exec the real watcher. The watcher's own interpreter self-heal (os.execv)
|
|
84
|
+
# keeps the new session.
|
|
85
|
+
nohup "${PYBIN}" -c 'import os, sys
|
|
86
|
+
try:
|
|
87
|
+
os.setsid()
|
|
88
|
+
except OSError:
|
|
89
|
+
pass
|
|
90
|
+
os.execv(sys.argv[1], sys.argv[1:])' "${PYBIN}" "${OVERLAY_PY}" watch >>"${WATCH_LOG}" 2>&1 &
|
|
77
91
|
disown 2>/dev/null || true
|
|
78
92
|
exit 0
|
package/bin/scheduler/systemd.js
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const { execSync, spawnSync } = require('child_process');
|
|
6
|
-
|
|
7
|
-
const UNIT_PREFIX = 'com.m13v.social-';
|
|
8
|
-
const SERVICE_SUFFIX = '.service';
|
|
9
|
-
const TIMER_SUFFIX = '.timer';
|
|
10
|
-
|
|
11
|
-
function renderService(job, env) {
|
|
12
|
-
return `[Unit]
|
|
13
|
-
Description=${job.label}
|
|
14
|
-
|
|
15
|
-
[Service]
|
|
16
|
-
Type=oneshot
|
|
17
|
-
ExecStart=/bin/bash ${job.script}
|
|
18
|
-
Environment=PATH=${env.path}
|
|
19
|
-
Environment=HOME=${env.home}
|
|
20
|
-
StandardOutput=append:${job.stdoutLog}
|
|
21
|
-
StandardError=append:${job.stderrLog}
|
|
22
|
-
`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function renderTimer(job) {
|
|
26
|
-
const onActive = job.runAtLoad ? 0 : job.interval;
|
|
27
|
-
return `[Unit]
|
|
28
|
-
Description=Timer for ${job.label}
|
|
29
|
-
|
|
30
|
-
[Timer]
|
|
31
|
-
OnActiveSec=${onActive}s
|
|
32
|
-
OnUnitActiveSec=${job.interval}s
|
|
33
|
-
Unit=${job.label}.service
|
|
34
|
-
|
|
35
|
-
[Install]
|
|
36
|
-
WantedBy=timers.target
|
|
37
|
-
`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function unitBase(job) {
|
|
41
|
-
return job.file.replace(/\.plist$/, '').replace(/\.service$|\.timer$/, '');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function generate({ jobs, outDir, env }) {
|
|
45
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
46
|
-
const written = [];
|
|
47
|
-
for (const job of jobs) {
|
|
48
|
-
const base = unitBase(job);
|
|
49
|
-
const serviceTarget = path.join(outDir, `${base}${SERVICE_SUFFIX}`);
|
|
50
|
-
const timerTarget = path.join(outDir, `${base}${TIMER_SUFFIX}`);
|
|
51
|
-
fs.writeFileSync(serviceTarget, renderService(job, env));
|
|
52
|
-
fs.writeFileSync(timerTarget, renderTimer(job));
|
|
53
|
-
written.push(serviceTarget, timerTarget);
|
|
54
|
-
}
|
|
55
|
-
return written;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function defaultEnv({ home, nodeBin }) {
|
|
59
|
-
const dirs = new Set();
|
|
60
|
-
if (nodeBin) dirs.add(nodeBin);
|
|
61
|
-
dirs.add('/usr/local/bin');
|
|
62
|
-
dirs.add('/usr/bin');
|
|
63
|
-
dirs.add('/bin');
|
|
64
|
-
return {
|
|
65
|
-
home,
|
|
66
|
-
path: [...dirs].join(':'),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ─────────────────────────── Control plane ───────────────────────────
|
|
71
|
-
|
|
72
|
-
function list() {
|
|
73
|
-
const loadedLabels = new Set();
|
|
74
|
-
const pidByLabel = new Map();
|
|
75
|
-
try {
|
|
76
|
-
const out = execSync(
|
|
77
|
-
'systemctl --user list-units --type=timer --all --no-legend --plain',
|
|
78
|
-
{ stdio: 'pipe', maxBuffer: 8 * 1024 * 1024 }
|
|
79
|
-
).toString();
|
|
80
|
-
for (const line of out.split('\n')) {
|
|
81
|
-
const parts = line.trim().split(/\s+/);
|
|
82
|
-
if (!parts.length) continue;
|
|
83
|
-
const unit = parts[0];
|
|
84
|
-
if (!unit.startsWith(UNIT_PREFIX) || !unit.endsWith(TIMER_SUFFIX)) continue;
|
|
85
|
-
const label = unit.slice(0, -TIMER_SUFFIX.length);
|
|
86
|
-
loadedLabels.add(label);
|
|
87
|
-
}
|
|
88
|
-
} catch {}
|
|
89
|
-
for (const label of loadedLabels) {
|
|
90
|
-
const pid = pidFor(label);
|
|
91
|
-
if (pid != null) pidByLabel.set(label, pid);
|
|
92
|
-
}
|
|
93
|
-
return { loadedLabels, pidByLabel };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function isLoaded(label) {
|
|
97
|
-
const r = spawnSync(
|
|
98
|
-
'systemctl',
|
|
99
|
-
['--user', 'is-enabled', `${label}${TIMER_SUFFIX}`],
|
|
100
|
-
{ encoding: 'utf8' }
|
|
101
|
-
);
|
|
102
|
-
const s = (r.stdout || '').trim();
|
|
103
|
-
return s === 'enabled' || s === 'static' || s === 'alias';
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function pidFor(label) {
|
|
107
|
-
try {
|
|
108
|
-
const out = execSync(
|
|
109
|
-
`systemctl --user show -p MainPID --value ${label}${SERVICE_SUFFIX}`,
|
|
110
|
-
{ stdio: 'pipe' }
|
|
111
|
-
).toString().trim();
|
|
112
|
-
const pid = parseInt(out, 10);
|
|
113
|
-
if (!isNaN(pid) && pid > 0) return pid;
|
|
114
|
-
return null;
|
|
115
|
-
} catch { return null; }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function labelFromUnitPath(unitPath) {
|
|
119
|
-
const base = path.basename(unitPath).replace(/\.(service|timer)$/, '');
|
|
120
|
-
return base;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// On systemd, "loading" means enabling + starting the timer. The unit file
|
|
124
|
-
// must be installed into the user's systemd dir (via `install`) first;
|
|
125
|
-
// `systemctl --user enable` expects to find the file under its search path.
|
|
126
|
-
function load(unitPath) {
|
|
127
|
-
const label = labelFromUnitPath(unitPath);
|
|
128
|
-
const reload = spawnSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf8' });
|
|
129
|
-
if (reload.status !== 0) {
|
|
130
|
-
return { ok: false, stderr: (reload.stderr || '').trim(), status: reload.status };
|
|
131
|
-
}
|
|
132
|
-
const r = spawnSync(
|
|
133
|
-
'systemctl',
|
|
134
|
-
['--user', 'enable', '--now', `${label}${TIMER_SUFFIX}`],
|
|
135
|
-
{ encoding: 'utf8' }
|
|
136
|
-
);
|
|
137
|
-
const stderr = (r.stderr || '').trim();
|
|
138
|
-
return { ok: r.status === 0, stderr, status: r.status };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function unload(label, unitPath) {
|
|
142
|
-
const lbl = label || labelFromUnitPath(unitPath);
|
|
143
|
-
const r = spawnSync(
|
|
144
|
-
'systemctl',
|
|
145
|
-
['--user', 'disable', '--now', `${lbl}${TIMER_SUFFIX}`],
|
|
146
|
-
{ encoding: 'utf8' }
|
|
147
|
-
);
|
|
148
|
-
const stderr = (r.stderr || '').trim();
|
|
149
|
-
return { ok: r.status === 0, stderr, status: r.status };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function kickstart(label) {
|
|
153
|
-
const r = spawnSync(
|
|
154
|
-
'systemctl',
|
|
155
|
-
['--user', 'start', `${label}${SERVICE_SUFFIX}`],
|
|
156
|
-
{ encoding: 'utf8' }
|
|
157
|
-
);
|
|
158
|
-
const stderr = (r.stderr || '').trim();
|
|
159
|
-
const pid = pidFor(label);
|
|
160
|
-
return { ok: r.status === 0, stderr, pid };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function killJob(label) {
|
|
164
|
-
const r = spawnSync(
|
|
165
|
-
'systemctl',
|
|
166
|
-
['--user', 'kill', '--signal=SIGKILL', `${label}${SERVICE_SUFFIX}`],
|
|
167
|
-
{ encoding: 'utf8' }
|
|
168
|
-
);
|
|
169
|
-
return { ok: r.status === 0, stderr: (r.stderr || '').trim() };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Install unit files into the user's systemd dir (via symlink). A systemd
|
|
173
|
-
// "unit" is actually a pair (.service + .timer) so we resolve both and
|
|
174
|
-
// install whichever siblings exist next to the given source file.
|
|
175
|
-
function install(unitSrc, agentsDir) {
|
|
176
|
-
fs.mkdirSync(agentsDir, { recursive: true });
|
|
177
|
-
const base = labelFromUnitPath(unitSrc);
|
|
178
|
-
const srcDir = path.dirname(unitSrc);
|
|
179
|
-
const linked = [];
|
|
180
|
-
for (const suffix of [SERVICE_SUFFIX, TIMER_SUFFIX]) {
|
|
181
|
-
const src = path.join(srcDir, `${base}${suffix}`);
|
|
182
|
-
if (!fs.existsSync(src)) continue;
|
|
183
|
-
const link = path.join(agentsDir, `${base}${suffix}`);
|
|
184
|
-
if (!fs.existsSync(link)) {
|
|
185
|
-
try { fs.symlinkSync(src, link); } catch { return null; }
|
|
186
|
-
}
|
|
187
|
-
linked.push(link);
|
|
188
|
-
}
|
|
189
|
-
return linked.length ? linked[linked.length - 1] : null;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function unitFileName(jobFile) {
|
|
193
|
-
// launchd jobFile is "com.m13v.social-X.plist"; for systemd the timer
|
|
194
|
-
// is the primary unit since that's what schedules the service.
|
|
195
|
-
const base = jobFile.replace(/\.plist$/, '').replace(/\.(service|timer)$/, '');
|
|
196
|
-
return `${base}${TIMER_SUFFIX}`;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Discover every social-autoposter job by scanning for .service files in
|
|
200
|
-
// either the repo's systemd/ dir or the user's systemd user dir.
|
|
201
|
-
function discoverJobs({ repoUnitDir, agentsDir }) {
|
|
202
|
-
const byLabel = new Map();
|
|
203
|
-
const scan = (dir) => {
|
|
204
|
-
try {
|
|
205
|
-
const files = fs.readdirSync(dir).filter(f =>
|
|
206
|
-
f.startsWith(UNIT_PREFIX) && f.endsWith(SERVICE_SUFFIX)
|
|
207
|
-
);
|
|
208
|
-
for (const f of files) {
|
|
209
|
-
try {
|
|
210
|
-
const body = fs.readFileSync(path.join(dir, f), 'utf8');
|
|
211
|
-
const { label, scriptPath } = parseUnit(body);
|
|
212
|
-
const resolvedLabel = label || f.slice(0, -SERVICE_SUFFIX.length);
|
|
213
|
-
if (!byLabel.has(resolvedLabel)) {
|
|
214
|
-
byLabel.set(resolvedLabel, {
|
|
215
|
-
label: resolvedLabel,
|
|
216
|
-
unitFile: `${resolvedLabel}${TIMER_SUFFIX}`,
|
|
217
|
-
scriptPath,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
} catch {}
|
|
221
|
-
}
|
|
222
|
-
} catch {}
|
|
223
|
-
};
|
|
224
|
-
scan(repoUnitDir);
|
|
225
|
-
scan(agentsDir);
|
|
226
|
-
return [...byLabel.values()];
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function parseUnit(text) {
|
|
230
|
-
// For a .service file: Description= is the label, ExecStart= points at
|
|
231
|
-
// the script. systemd ExecStart can be "/bin/bash /path/to/x.sh" or
|
|
232
|
-
// just the script itself.
|
|
233
|
-
const descM = text.match(/^Description=(.+)$/m);
|
|
234
|
-
const label = descM ? descM[1].trim() : null;
|
|
235
|
-
let scriptPath = null;
|
|
236
|
-
const execM = text.match(/^ExecStart=(.+)$/m);
|
|
237
|
-
if (execM) {
|
|
238
|
-
const tokens = execM[1].trim().split(/\s+/);
|
|
239
|
-
scriptPath = tokens.find(s => /\.(sh|py|js)$/.test(s)) || tokens[tokens.length - 1] || null;
|
|
240
|
-
}
|
|
241
|
-
return { label, scriptPath };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// For systemd, callers pass the TIMER file contents (that's where the
|
|
245
|
-
// schedule lives). Returns interval in seconds, or null if calendar-based
|
|
246
|
-
// and not reducible to a single gap.
|
|
247
|
-
function scheduleFromUnit(text) {
|
|
248
|
-
try {
|
|
249
|
-
const unitActive = text.match(/^OnUnitActiveSec=(\d+)(s|sec|m|min|h|hour)?$/m);
|
|
250
|
-
if (unitActive) {
|
|
251
|
-
return { intervalSecs: toSeconds(unitActive[1], unitActive[2]), kind: 'simple' };
|
|
252
|
-
}
|
|
253
|
-
const active = text.match(/^OnActiveSec=(\d+)(s|sec|m|min|h|hour)?$/m);
|
|
254
|
-
if (active) {
|
|
255
|
-
return { intervalSecs: toSeconds(active[1], active[2]), kind: 'simple' };
|
|
256
|
-
}
|
|
257
|
-
const calendars = [...text.matchAll(/^OnCalendar=(.+)$/gm)].map(m => m[1].trim());
|
|
258
|
-
if (calendars.length) {
|
|
259
|
-
return { intervalSecs: null, kind: 'calendar' };
|
|
260
|
-
}
|
|
261
|
-
return { intervalSecs: null, kind: null };
|
|
262
|
-
} catch { return { intervalSecs: null, kind: null }; }
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function toSeconds(num, unit) {
|
|
266
|
-
const n = parseInt(num, 10);
|
|
267
|
-
switch ((unit || 's').toLowerCase()) {
|
|
268
|
-
case 's': case 'sec': return n;
|
|
269
|
-
case 'm': case 'min': return n * 60;
|
|
270
|
-
case 'h': case 'hour': return n * 3600;
|
|
271
|
-
default: return n;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// In-place edit of the interval on a timer file. Returns true if updated,
|
|
276
|
-
// false if the unit uses OnCalendar= (calendar schedule; not settable here).
|
|
277
|
-
function updateInterval(unitPath, seconds) {
|
|
278
|
-
const text = fs.readFileSync(unitPath, 'utf8');
|
|
279
|
-
if (/^OnCalendar=/m.test(text) && !/^OnUnitActiveSec=/m.test(text)) return false;
|
|
280
|
-
let next = text;
|
|
281
|
-
if (/^OnUnitActiveSec=/m.test(next)) {
|
|
282
|
-
next = next.replace(/^OnUnitActiveSec=.*$/m, `OnUnitActiveSec=${seconds}s`);
|
|
283
|
-
} else {
|
|
284
|
-
// No schedule line present — cannot infer what to change safely.
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
287
|
-
if (/^OnActiveSec=/m.test(next)) {
|
|
288
|
-
next = next.replace(/^OnActiveSec=.*$/m, `OnActiveSec=${seconds}s`);
|
|
289
|
-
}
|
|
290
|
-
fs.writeFileSync(unitPath, next);
|
|
291
|
-
return true;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
module.exports = {
|
|
295
|
-
renderService,
|
|
296
|
-
renderTimer,
|
|
297
|
-
generate,
|
|
298
|
-
defaultEnv,
|
|
299
|
-
unitBase,
|
|
300
|
-
list,
|
|
301
|
-
isLoaded,
|
|
302
|
-
pidFor,
|
|
303
|
-
load,
|
|
304
|
-
unload,
|
|
305
|
-
kickstart,
|
|
306
|
-
killJob,
|
|
307
|
-
install,
|
|
308
|
-
unitFileName,
|
|
309
|
-
discoverJobs,
|
|
310
|
-
parseUnit,
|
|
311
|
-
scheduleFromUnit,
|
|
312
|
-
updateInterval,
|
|
313
|
-
};
|