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 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 kind = platform.scheduler();
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 ${kind} units at ${outDir}`);
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
 
@@ -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, systemd };
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
- \t<key>StartInterval</key>
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
@@ -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>
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.6.164",
3
- "installedAt": "2026-06-30T00:37:08.142Z"
2
+ "version": "1.6.166",
3
+ "installedAt": "2026-06-30T02:20:22.438Z"
4
4
  }
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.164",
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.164",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "social-autoposter",
3
- "version": "1.6.164",
3
+ "version": "1.6.166",
4
4
  "description": "Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Install as a Claude Code agent skill.",
5
5
  "bin": {
6
6
  "social-autoposter": "bin/cli.js"
@@ -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, or run the draft-only verification. Do them.
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
- Draft-only verification (the autopilot drafts without posting) is safe and
30
- required.
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 — a
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. Poll the `dashboard` tool until the pending-draft count rises; a
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. Verify without posting:
199
- `DRAFT_ONLY=1 TWITTER_PAGE_GEN_RATE=0 bash ~/social-autoposter/skill/run-twitter-cycle.sh`
200
- 8. Do not load launchd/autopilot jobs unless explicitly requested.
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
- nohup "${PYBIN}" "${OVERLAY_PY}" watch >>"${WATCH_LOG}" 2>&1 &
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
@@ -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
- };