social-autoposter 1.6.75 → 1.6.78

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/mcp/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import fs from "node:fs";
19
19
  import { repoDir, runPython, run, readPlan, writePlan, planPath, scanResultPath, } from "./repo.js";
20
20
  import { applySetup, resolveProject, listManagedProjectStatus, REQUIRED_FIELDS, RECOMMENDED_FIELDS, configPath, } from "./setup.js";
21
21
  import { xStatus, xConnect, xDetectSources, xScanProfile, summarizeXAuth } from "./twitterAuth.js";
22
- import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, resolveChrome, ensureMenubar, } from "./runtime.js";
22
+ import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, resolveChrome, ensureMenubar, ensurePipelineCurrent, } from "./runtime.js";
23
23
  import { blockOnboardingMilestone, completeOnboardingMilestone, ensureDoctorPhase, onboardingLedger, onboardingSnapshot, recordOnboardingAttempt, runDoctorPhase, } from "./onboarding.js";
24
24
  import { VERSION, versionStatus, latestPublishedVersion } from "./version.js";
25
25
  import { initSentry, sendHeartbeat, captureError, flushSentry } from "./telemetry.js";
@@ -30,6 +30,7 @@ import http from "node:http";
30
30
  // (vite-plugin-singlefile) into dist/panel.html alongside this compiled file.
31
31
  const DIST_DIR = path.dirname(fileURLToPath(import.meta.url));
32
32
  const PANEL_URI = "ui://social-autoposter/panel.html";
33
+ const PRODUCT_LINK_URI = "ui://social-autoposter/product-link.html";
33
34
  // Stable id for the accumulating draft review queue. submit_drafts appends each
34
35
  // run's drafts here (dedup by tweet URL) so the menu-bar cards PILE UP across a
35
36
  // continuous autopilot instead of each run overwriting the last; post_drafts posts
@@ -151,7 +152,7 @@ async function unloadPlist(label, plistPath, uid) {
151
152
  // so serverInfo.version finally reflects what the user actually has installed
152
153
  // instead of a frozen literal.
153
154
  const server = new McpServer({
154
- name: "social-autoposter",
155
+ name: "S4L",
155
156
  version: VERSION,
156
157
  }, {
157
158
  // Host injects this into the agent's system prompt, so be proactive about
@@ -182,12 +183,18 @@ const server = new McpServer({
182
183
  "autopilot by calling the HOST tool `create_scheduled_task` with: taskId " +
183
184
  "'social-autoposter-autopilot'; cronExpression '* * * * *' (continuous, like the pipeline; the " +
184
185
  "host runs one at a time and skips overlapping fires, so cycles run back-to-back); and a " +
185
- "self-contained prompt telling the run to call scan_candidates, draft ONE on-brand reply " +
186
- "(<=250 chars, same language as the thread, adds real value) for each candidate worth engaging, " +
187
- "then call submit_drafts with those drafts, and NOT post (drafts pile up in the menu-bar " +
188
- "approval cards for the user to approve). Then run ONE cycle yourself now (scan_candidates -> " +
189
- "draft -> submit_drafts) to seed the cards and pre-approve the tools, so unattended fires do not " +
190
- "stall on a permission prompt. If the host exposes no create_scheduled_task tool, tell the user " +
186
+ "self-contained prompt telling the run to call scan_candidates (which long-polls if it " +
187
+ "returns a `Scan in progress` status, call scan_candidates again with the same args and keep " +
188
+ "re-calling until it returns candidates; never sleep or use background waits between calls), " +
189
+ "then (if it needs the project's brand voice to draft on-brand) call project_config to read it, " +
190
+ "draft ONE on-brand reply (<=250 chars, same language as the thread, adds real value) for each " +
191
+ "candidate worth engaging, then call submit_drafts with those drafts, and NOT post (drafts pile " +
192
+ "up in the menu-bar approval cards for the user to approve). The prompt MUST name EVERY tool the " +
193
+ "run can use (scan_candidates, project_config, submit_drafts) so none is a surprise mid-run. " +
194
+ "Then run ONE FULL cycle yourself now that exercises ALL of them " +
195
+ "(scan_candidates -> project_config -> draft -> submit_drafts) to seed the cards AND pre-approve " +
196
+ "every tool, so unattended fires never stall waiting on a per-tool permission prompt. If the host " +
197
+ "exposes no create_scheduled_task tool, tell the user " +
191
198
  "the autopilot could not be scheduled here and to run a draft cycle manually instead.\n\n" +
192
199
  "BE PROACTIVE ABOUT MARKETING MOMENTS. Whenever the user ships, finishes, merges, or releases " +
193
200
  "something worth talking about in this session (a new feature, a launch, a long-awaited fix, a " +
@@ -524,6 +531,10 @@ async function postApproved(batchId, plan) {
524
531
  const approved = (plan.candidates || []).filter((c) => c.approved === true);
525
532
  if (approved.length === 0)
526
533
  return { attempted: 0, exit_code: 0, summary: "nothing approved" };
534
+ // Posting is a priority over scanning: abort any in-flight plugin scan so the
535
+ // approved post takes the browser immediately instead of waiting on the lock.
536
+ // Plugin pipeline only — never affects the plist autopilot.
537
+ preemptScanForPost();
527
538
  const approvedBatch = `${batchId}_approved`;
528
539
  writePlan(approvedBatch, { ...plan, candidates: approved });
529
540
  // SAPS_SKIP_CAMPAIGN_SUFFIX=1: manual/reviewed posts from this MCP draft_cycle
@@ -590,7 +601,7 @@ async function postApproved(batchId, plan) {
590
601
  // drift from REQUIRED_FIELDS / the project_config tool's flow. All real logic stays
591
602
  // in `project_config`; this is just a convenience handle to begin.
592
603
  server.registerPrompt("getting-started", {
593
- title: "Set up social-autoposter",
604
+ title: "Set up S4L",
594
605
  description: "Start here. Walks you through configuring a product and connecting your X/Twitter " +
595
606
  "account so the autoposter can draft and post for you.",
596
607
  }, async () => ({
@@ -1475,13 +1486,16 @@ let localPanel = null;
1475
1486
  // Read the built panel.html and flip it into HTTP-bridge mode by injecting a
1476
1487
  // flag the front-end reads at boot. Same bytes as the inline ui:// resource,
1477
1488
  // minus the postMessage host (there's none over loopback).
1478
- function panelHtmlForHttp() {
1479
- const html = fs.readFileSync(path.join(DIST_DIR, "panel.html"), "utf-8");
1489
+ function widgetHtmlForHttp(file) {
1490
+ const html = fs.readFileSync(path.join(DIST_DIR, file), "utf-8");
1480
1491
  const inject = `<script>window.__SAPS_BRIDGE__=${JSON.stringify("http")};</script>`;
1481
1492
  if (html.includes("</head>"))
1482
1493
  return html.replace("</head>", inject + "</head>");
1483
1494
  return inject + html;
1484
1495
  }
1496
+ function panelHtmlForHttp() {
1497
+ return widgetHtmlForHttp("panel.html");
1498
+ }
1485
1499
  function readBody(req) {
1486
1500
  return new Promise((resolve, reject) => {
1487
1501
  const chunks = [];
@@ -1506,6 +1520,12 @@ function startLocalPanel() {
1506
1520
  res.end(panelHtmlForHttp());
1507
1521
  return;
1508
1522
  }
1523
+ if (req.method === "GET" &&
1524
+ (url.pathname === "/product-link" || url.pathname === "/product-link.html")) {
1525
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1526
+ res.end(widgetHtmlForHttp("product-link.html"));
1527
+ return;
1528
+ }
1509
1529
  if (req.method === "GET" && url.pathname === "/health") {
1510
1530
  res.writeHead(200, { "Content-Type": "application/json" });
1511
1531
  res.end(JSON.stringify({ ok: true }));
@@ -1660,9 +1680,20 @@ async function runScanCandidates(project, onProgress) {
1660
1680
  env.BH_CHROME_BIN = chrome;
1661
1681
  let step = 0;
1662
1682
  let lastMsg = "";
1683
+ // Specific phase label for the menu bar for the whole multi-minute scan, so the
1684
+ // long-poll doesn't leave it on a generic "working" (or flicker to idle between
1685
+ // re-calls). Cleared by the handler when the scan resolves.
1686
+ writeActivity("scanning", project ? `scanning X for ${project}` : "scanning X");
1663
1687
  const res = await run("bash", ["skill/run-twitter-cycle.sh"], {
1664
1688
  env,
1665
1689
  timeoutMs: 900_000,
1690
+ onSpawn: (c) => {
1691
+ // Track the PLUGIN's own scan process so an approved post can abort exactly
1692
+ // this scan (posting preempts scanning). This is the plugin pipeline only —
1693
+ // the plist autopilot's scan is a separate launchd process the MCP server
1694
+ // never spawns and has no handle to, so it can never be touched here.
1695
+ scanChild = c;
1696
+ },
1666
1697
  onLine: (line) => {
1667
1698
  const t = line.replace(/\s+$/, "");
1668
1699
  if (t.trim())
@@ -1676,6 +1707,7 @@ async function runScanCandidates(project, onProgress) {
1676
1707
  }
1677
1708
  },
1678
1709
  });
1710
+ scanChild = null; // scan finished on its own; nothing to preempt.
1679
1711
  const marker = /SCAN_ONLY_RESULT=(\/\S+\.json)/.exec(res.stdout + "\n" + res.stderr);
1680
1712
  if (marker && marker[1]) {
1681
1713
  try {
@@ -1699,6 +1731,20 @@ async function runScanCandidates(project, onProgress) {
1699
1731
  if (blockedMarker && blockedMarker[1]) {
1700
1732
  return { batchId: null, candidates: [], blocked: blockedReasonMessage(blockedMarker[1]) };
1701
1733
  }
1734
+ // The pipeline's single-flight guard prints too_many_inflight and exits 0 when
1735
+ // ANOTHER scan already holds the twitter-cycle slot (commonly the legacy
1736
+ // claude -p autopilot, which we deliberately keep running). That is CONTENTION,
1737
+ // not an empty batch — flag it so the caller retries instead of concluding
1738
+ // "no candidates" (a clean empty scan returns a batch_id with count:0).
1739
+ if (/too_many_inflight/.test(res.stdout + res.stderr)) {
1740
+ return {
1741
+ batchId: null,
1742
+ candidates: [],
1743
+ busy: true,
1744
+ blocked: "Another scan already holds the pipeline slot (likely the legacy autopilot). Contention, " +
1745
+ "not an empty result — retry scan_candidates shortly.",
1746
+ };
1747
+ }
1702
1748
  return {
1703
1749
  batchId: null,
1704
1750
  candidates: [],
@@ -1707,17 +1753,92 @@ async function runScanCandidates(project, onProgress) {
1707
1753
  res.stderr.split("\n").slice(-12).join("\n"),
1708
1754
  };
1709
1755
  }
1710
- tool("scan_candidates", {
1711
- title: "Scan X for reply candidates (no drafting, no posting)",
1712
- description: "Step 1 of a hands-free / scheduled autopilot run. Runs the scan+score half of the pipeline " +
1713
- "and returns the top scored X/Twitter threads worth replying to — WITHOUT drafting or posting, " +
1714
- "and without spending any `claude -p` budget. You (this session) then draft an on-brand reply " +
1715
- "for each good candidate YOURSELF and submit them with `submit_drafts`. Each candidate includes " +
1716
- "its candidate_id (pass it back), the thread text/author, the matched project, and engagement " +
1717
- "metrics. Optional `project` scopes the scan to one configured project.",
1718
- inputSchema: { project: z.string().optional() },
1719
- }, async (args) => {
1720
- const scan = await runScanCandidates(args?.project);
1756
+ // At most one scan runs at a time (mirrors the pipeline's own max=1 lock). Kept
1757
+ // across calls so a later poll attaches to the SAME running scan instead of
1758
+ // starting a new one.
1759
+ let currentScanJob = null;
1760
+ // The PLUGIN's currently-spawned scan subprocess (captured via run()'s onSpawn),
1761
+ // so an approved post can abort exactly this scan and take the browser
1762
+ // immediately. Only ever references a scan the MCP server itself launched never
1763
+ // the plist autopilot's launchd scan.
1764
+ let scanChild = null;
1765
+ // Posting takes priority over scanning (plugin pipeline only). When the user
1766
+ // approves a post, abort any in-flight plugin scan so the browser frees up at
1767
+ // once: killing the scan's bash leaves a dead lock-holder pid that the poster's
1768
+ // twitter_browser.py steals via its liveness check, so the post acquires the
1769
+ // browser without the 45s lock wait. The aborted scan just re-runs next cron
1770
+ // tick. Best-effort; never throws. Does NOT touch the plist autopilot (separate
1771
+ // process, no handle here) or any locked pipeline script.
1772
+ function preemptScanForPost() {
1773
+ try {
1774
+ if (scanChild && scanChild.pid && scanChild.exitCode === null) {
1775
+ console.error(`[post] preempting in-flight plugin scan (pid ${scanChild.pid}) so the approved post takes the browser`);
1776
+ try {
1777
+ scanChild.kill("SIGTERM");
1778
+ }
1779
+ catch {
1780
+ /* already gone */
1781
+ }
1782
+ }
1783
+ }
1784
+ catch {
1785
+ /* best effort */
1786
+ }
1787
+ // Drop the long-poll job so a queued poller stops waiting on the aborted scan;
1788
+ // the next scan_candidates call starts fresh.
1789
+ currentScanJob = null;
1790
+ scanChild = null;
1791
+ }
1792
+ // Per-call long-poll window. Must stay under the ~60s MCP client request timeout
1793
+ // so every scan_candidates call returns a real response before the client gives
1794
+ // up. Override with SAPS_SCAN_POLL_WAIT_MS for non-standard clients.
1795
+ const SCAN_POLL_WAIT_MS = Number(process.env.SAPS_SCAN_POLL_WAIT_MS) || 45_000;
1796
+ // After a contention hit (too_many_inflight), wait this long before launching a
1797
+ // fresh scan, so polls don't hammer the pipeline with back-to-back no-op scans
1798
+ // while the other holder finishes. Polls during the cooldown just report "busy".
1799
+ const SCAN_BUSY_COOLDOWN_MS = Number(process.env.SAPS_SCAN_BUSY_COOLDOWN_MS) || 20_000;
1800
+ let scanBusyUntil = 0;
1801
+ function scanBusy() {
1802
+ return textContent("Another scan is already running on this machine (likely the legacy autopilot holding the " +
1803
+ "twitter-cycle slot). This is CONTENTION, not an empty result. Call scan_candidates again in " +
1804
+ "~20s to retry — do NOT conclude there are no candidates, and do NOT sleep; just re-call.");
1805
+ }
1806
+ // Resolve to {done:true,value} the moment `p` settles, or {done:false} after
1807
+ // `ms` — whichever is first. The job promise keeps running either way, so a
1808
+ // later poll re-attaches to it.
1809
+ function waitUpTo(p, ms) {
1810
+ return Promise.race([
1811
+ p.then((value) => ({ done: true, value })),
1812
+ new Promise((resolve) => setTimeout(() => resolve({ done: false }), ms)),
1813
+ ]);
1814
+ }
1815
+ function startScanJob(project) {
1816
+ const job = {
1817
+ project,
1818
+ startedAt: Date.now(),
1819
+ done: false,
1820
+ promise: undefined,
1821
+ };
1822
+ job.promise = runScanCandidates(project)
1823
+ .then((r) => {
1824
+ job.result = r;
1825
+ job.done = true;
1826
+ return r;
1827
+ })
1828
+ .catch((e) => {
1829
+ const r = {
1830
+ batchId: null,
1831
+ candidates: [],
1832
+ blocked: `The scan crashed before producing a result: ${String(e?.message || e)}. ` +
1833
+ `Call scan_candidates again to retry.`,
1834
+ };
1835
+ job.result = r;
1836
+ job.done = true;
1837
+ return r;
1838
+ });
1839
+ return job;
1840
+ }
1841
+ function formatScanResult(scan) {
1721
1842
  if (!scan.batchId) {
1722
1843
  return textContent(scan.blocked || "No candidates found.");
1723
1844
  }
@@ -1730,6 +1851,62 @@ tool("scan_candidates", {
1730
1851
  `"${scan.batchId}" and one entry per drafted reply ({candidate_id, reply_text}). Nothing posts ` +
1731
1852
  `until the user approves.`,
1732
1853
  });
1854
+ }
1855
+ function scanInProgress(job) {
1856
+ const elapsed = Math.round((Date.now() - job.startedAt) / 1000);
1857
+ return textContent(`Scan in progress (${elapsed}s elapsed). The X scan drives a real browser and usually takes ` +
1858
+ `1-3 minutes. Call scan_candidates again RIGHT NOW to keep waiting — each call long-polls up ` +
1859
+ `to ~${Math.round(SCAN_POLL_WAIT_MS / 1000)}s and returns the candidates the moment the scan ` +
1860
+ `finishes. Do NOT sleep, run a background command, or do other work to bridge the gap; just ` +
1861
+ `re-call scan_candidates until it returns candidates or reports no candidates.`);
1862
+ }
1863
+ tool("scan_candidates", {
1864
+ title: "Scan X for reply candidates (no drafting, no posting)",
1865
+ description: "Step 1 of a hands-free / scheduled autopilot run. Runs the scan+score half of the pipeline " +
1866
+ "and returns the top scored X/Twitter threads worth replying to — WITHOUT drafting or posting, " +
1867
+ "and without spending any `claude -p` budget. You (this session) then draft an on-brand reply " +
1868
+ "for each good candidate YOURSELF and submit them with `submit_drafts`. Each candidate includes " +
1869
+ "its candidate_id (pass it back), the thread text/author, the matched project, and engagement " +
1870
+ "metrics. Optional `project` scopes the scan to one configured project. The scan drives a real " +
1871
+ "browser and can take 1-3 minutes: this call long-polls and may return a `Scan in progress` " +
1872
+ "status instead of candidates — if so, just call scan_candidates again (same args) and keep " +
1873
+ "re-calling until it returns candidates. Never sleep or use a background wait to bridge the gap.",
1874
+ inputSchema: { project: z.string().optional() },
1875
+ }, async (args) => {
1876
+ // Long-poll the single in-flight scan job (see the ScanJob registry above).
1877
+ // A finished-but-unconsumed job: hand back its result and clear the slot so a
1878
+ // later call starts a fresh scan.
1879
+ if (currentScanJob?.done) {
1880
+ const finished = currentScanJob;
1881
+ currentScanJob = null;
1882
+ if (finished.result?.busy) {
1883
+ scanBusyUntil = Date.now() + SCAN_BUSY_COOLDOWN_MS;
1884
+ return scanBusy();
1885
+ }
1886
+ return formatScanResult(finished.result);
1887
+ }
1888
+ // Contention cooldown: another scan holds the slot — report busy without
1889
+ // launching yet another no-op scan against the locked pipeline.
1890
+ if (!currentScanJob && Date.now() < scanBusyUntil) {
1891
+ return scanBusy();
1892
+ }
1893
+ // Nothing running -> start one scan (single-flight: never spawn a second
1894
+ // run-twitter-cycle.sh that would just hit the pipeline's too_many_inflight).
1895
+ if (!currentScanJob) {
1896
+ currentScanJob = startScanJob(args?.project);
1897
+ }
1898
+ // Wait for the in-flight scan, but no longer than the client timeout allows.
1899
+ const waited = await waitUpTo(currentScanJob.promise, SCAN_POLL_WAIT_MS);
1900
+ if (waited.done) {
1901
+ currentScanJob = null;
1902
+ if (waited.value.busy) {
1903
+ scanBusyUntil = Date.now() + SCAN_BUSY_COOLDOWN_MS;
1904
+ return scanBusy();
1905
+ }
1906
+ return formatScanResult(waited.value);
1907
+ }
1908
+ // Still scanning — tell the agent to simply call scan_candidates again.
1909
+ return scanInProgress(currentScanJob);
1733
1910
  });
1734
1911
  tool("submit_drafts", {
1735
1912
  title: "Submit drafted replies for review",
@@ -1837,8 +2014,8 @@ tool("submit_drafts", {
1837
2014
  `"${REVIEW_QUEUE_ID}" and the numbers to post.`);
1838
2015
  });
1839
2016
  appTool("dashboard", {
1840
- title: "Social Autoposter dashboard",
1841
- description: "Render the Social Autoposter dashboard in chat: a visual surface showing project setup, X " +
2017
+ title: "S4L dashboard",
2018
+ description: "Render the S4L dashboard in chat: a visual surface showing project setup, X " +
1842
2019
  "connection, autopilot state, and 7-day stats, with buttons to run a draft cycle, connect X, " +
1843
2020
  "and refresh. Use when the user asks to see the dashboard, panel, " +
1844
2021
  "status, or controls. ALSO call this at the end of any state-changing or results-producing " +
@@ -1852,7 +2029,7 @@ appTool("dashboard", {
1852
2029
  _meta: { ui: { resourceUri: PANEL_URI } },
1853
2030
  }, async () => {
1854
2031
  const snap = await buildSnapshot();
1855
- const human = `Social Autoposter v${snap.version}` +
2032
+ const human = `S4L v${snap.version}` +
1856
2033
  (snap.update_available && snap.latest_version ? ` (update to ${snap.latest_version})` : "") +
1857
2034
  ` — projects ${snap.projects_ready}/${snap.projects_total} ready, ` +
1858
2035
  `X ${snap.x_connected ? "connected" : "not connected"}, ` +
@@ -1892,6 +2069,52 @@ appTool("dashboard", {
1892
2069
  return base;
1893
2070
  }
1894
2071
  });
2072
+ // ---- add your product: focused single-field onboarding widget --------------
2073
+ // A standalone ui:// widget (separate from the dashboard panel) that captures
2074
+ // the user's product URL. The widget itself reads project status and either
2075
+ // writes the website via project_config (callServerTool) or, on a cold start,
2076
+ // hands the URL to the model via sendMessage. Same inline/loopback duality as
2077
+ // `dashboard`.
2078
+ appTool("connect_product", {
2079
+ title: "Add your product",
2080
+ description: "Render the 'add your product' widget in chat: a single-field form where the user pastes " +
2081
+ "their product's website. Use at the START of onboarding when you need the product URL, " +
2082
+ "instead of asking for it in plain prose. If a project already needs a website the widget " +
2083
+ "saves it directly; on a cold start it kicks off end-to-end setup. Hosts without UI support " +
2084
+ "get a loopback URL.",
2085
+ inputSchema: {},
2086
+ outputSchema: { snapshot: z.string(), fallback_url: z.string().optional() },
2087
+ _meta: { ui: { resourceUri: PRODUCT_LINK_URI } },
2088
+ }, async () => {
2089
+ const snap = await buildSnapshot();
2090
+ // Inline-capable host: paint the resource named by _meta.ui.resourceUri.
2091
+ // Emit no text content so the chat shows only the widget (see `dashboard`).
2092
+ if (hostRendersAppUi()) {
2093
+ return { content: [], structuredContent: { snapshot: JSON.stringify(snap) } };
2094
+ }
2095
+ // No inline UI: serve the identical product-link.html from the loopback
2096
+ // server at /product-link and return its URL.
2097
+ try {
2098
+ const base = await startLocalPanel();
2099
+ const url = base.replace(/\/$/, "") + "/product-link";
2100
+ await openInBrowser(url);
2101
+ return {
2102
+ content: [{
2103
+ type: "text",
2104
+ text: "Add your product: paste your product's website to begin setup.\n\n" +
2105
+ `This host can't render the widget inline. Loopback URL: ${url}`,
2106
+ }],
2107
+ structuredContent: { snapshot: JSON.stringify(snap), fallback_url: url },
2108
+ };
2109
+ }
2110
+ catch (e) {
2111
+ console.error("[social-autoposter-mcp] product-link fallback failed:", e?.message || e);
2112
+ return {
2113
+ content: [{ type: "text", text: "Paste your product's website in the chat to begin setup." }],
2114
+ structuredContent: { snapshot: JSON.stringify(snap) },
2115
+ };
2116
+ }
2117
+ });
1895
2118
  // ---- show browser to user: live CDP screencast ----------------------------
1896
2119
  // Streams a live view of the autoposter's managed Chrome into the panel. Frames
1897
2120
  // travel back through the normal tool-result channel as a data: URL (which the
@@ -1963,7 +2186,7 @@ tool("show_browser_to_user", {
1963
2186
  frame: frame ? `data:image/jpeg;base64,${frame}` : null,
1964
2187
  });
1965
2188
  });
1966
- registerAppResource(server, "Social Autoposter panel", PANEL_URI, { mimeType: RESOURCE_MIME_TYPE }, async () => ({
2189
+ registerAppResource(server, "S4L panel", PANEL_URI, { mimeType: RESOURCE_MIME_TYPE }, async () => ({
1967
2190
  contents: [
1968
2191
  {
1969
2192
  uri: PANEL_URI,
@@ -1972,8 +2195,22 @@ registerAppResource(server, "Social Autoposter panel", PANEL_URI, { mimeType: RE
1972
2195
  },
1973
2196
  ],
1974
2197
  }));
2198
+ registerAppResource(server, "S4L product link", PRODUCT_LINK_URI, { mimeType: RESOURCE_MIME_TYPE }, async () => ({
2199
+ contents: [
2200
+ {
2201
+ uri: PRODUCT_LINK_URI,
2202
+ mimeType: RESOURCE_MIME_TYPE,
2203
+ text: fs.readFileSync(path.join(DIST_DIR, "product-link.html"), "utf-8"),
2204
+ },
2205
+ ],
2206
+ }));
1975
2207
  async function main() {
1976
2208
  initSentry();
2209
+ // A plugin UPDATE refreshes this server (dist/) but not the materialized
2210
+ // pipeline. Re-extract the bundled pipeline.tgz when it's newer than what's on
2211
+ // disk, BEFORE serving, so the very first scan uses the shipped pipeline (not
2212
+ // the version first materialized at install). Synchronous + best-effort.
2213
+ ensurePipelineCurrent();
1977
2214
  const transport = new StdioServerTransport();
1978
2215
  await server.connect(transport);
1979
2216
  console.error(`[social-autoposter-mcp] connected. v=${VERSION} repo=${repoDir()}`);
@@ -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:tt.describe("The display mode being requested.")})});var mh=m({mode:tt.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),ph=U([u("model"),u("app")]).describe("Tool visibility scope - who can access the tool.");m({resourceUri:d().optional(),visibility:z(ph).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:we().optional(),permissions:we().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(kt).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:fo.optional().describe("CSP configuration from resource metadata."),permissions:ho.optional().describe("Sandbox permissions from resource metadata.")})});var fh=m({method:u("ui/notifications/tool-result"),params:Fn.describe("Standard MCP tool execution result.")}),kl=m({toolInfo:m({id:gt.optional().describe("JSON-RPC id of the tools/call request."),tool:po.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:Yf.optional().describe("Current color theme preference."),styles:ch.optional().describe("Style configuration for theming the app."),displayMode:tt.optional().describe("How the UI is currently displayed."),availableDisplayModes:z(tt).optional().describe("Display modes the host supports."),containerDimensions:U([m({height:T().describe("Fixed container height in pixels.")}),m({maxHeight:U([T(),Ye()]).optional().describe("Maximum container height in pixels.")})]).and(U([m({width:T().describe("Fixed container width in pixels.")}),m({maxWidth:U([T(),Ye()]).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(),hh=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(kt).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:dh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var gh=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:qn.describe("Host application identification and version."),hostCapabilities:lh.describe("Features and capabilities provided by the host."),hostContext:kl.describe("Rich context about the host environment.")}).passthrough(),vh={target:"draft-2020-12"};async function Fo(t,n){let r=t["~standard"];if(r.jsonSchema)return r.jsonSchema[n](vh);if(r.vendor==="zod"){let{z:o}=await Ul(()=>Promise.resolve().then(()=>ip),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 _h(t){let n=document.documentElement;n.setAttribute("data-theme",t),n.style.colorScheme=t}function bh(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 Wf{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:ih,toolinputpartial:oh,toolresult:fh,toolcancelled:ah,hostcontextchanged:hh});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=Bf(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 y=c.inputSchema,w=y?await Jo(y,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(uh,(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?yl:$l;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},rh,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},Qi,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},th,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},nh,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},mh,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 Qf(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}},gh,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 tr=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 yh{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 kh(){return globalThis.__SAPS_BRIDGE__==="http"?new yh:new tr({name:"Social Autoposter 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 V=t=>document.getElementById(t),wl=V("ver"),Be=V("btn-setup"),We=V("btn-draft"),zt=V("stats-grid"),Vn=V("stats-toggle"),Sh=V("log"),wh=V("install-card"),ke=V("setup-summary"),Il=V("onboarding-details"),Ih=V("onboarding-steps"),Bn=V("onboarding-blocker"),Bo=V("onboarding-count"),zh=V("onboarding-bar-fill"),xh=V("live-card"),jh=V("stats-card"),Wo=V("install-steps"),Pe=V("install-err"),he=V("btn-install"),go=V("btn-live"),vo=V("btn-live-stop"),Go=V("btn-live-front"),Ue=V("live-status"),Zt=V("live-img");let W=null,Wn=!1,xt=!1,Gn=!1,Ee=!1,Je=!1;function Q(t){Sh.textContent=t}function Nh(t){switch(t){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function _o(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">${Nh(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),t.error?(Pe.textContent=t.error,Pe.hidden=!1):Pe.hidden=!0}const Th={environment_checked:"Environment checked",runtime_ready:"Runtime ready",x_connected:"X connected",profile_scanned:"Profile scanned",project_ready:"Project ready",topics_seeded:"Topics seeded",draft_verified:"Draft cycle verified"};function Oh(t){switch(t){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function zl(){Il.hidden=!Ee,ke.setAttribute("aria-expanded",String(Ee)),ke.classList.toggle("expanded",Ee)}function Ph(t){if(!t||!Array.isArray(t.milestones)){ke.hidden=!0,Il.hidden=!0;return}ke.hidden=!1;const n=t.milestones.length,r=t.milestones.filter(e=>e.status==="complete").length,o=!!t.current_blocker&&!t.complete;ke.classList.toggle("complete",t.complete),ke.classList.toggle("blocked",o),Bo.hidden=t.complete,Bo.textContent=o?`${r}/${n} · needs you`:xt?`${r}/${n} · setting up…`:`${r}/${n}`,zh.style.width=n>0?`${Math.round(r/n*100)}%`:"0%",Ih.innerHTML=t.milestones.map(e=>{const i=Th[e.id]||e.id,a=e.attempts>1?` <span class="detail">${e.attempts} attempts</span>`:"";return`<li class="${e.status}"><span class="glyph">${Oh(e.status)}</span><span>${i}${a}</span></li>`}).join(""),t.current_blocker?(Bn.textContent=`Current blocker: ${t.current_blocker.message}`,Bn.hidden=!1,Ee=!0):Bn.hidden=!0,zl()}function Ct(){if(!W)return;Ph(W.onboarding),wl.innerHTML=W.update_available&&W.latest_version?`v${W.version} · <button id="btn-update" class="update-btn">Update to ${W.latest_version}</button>`:`v${W.version}`;const t=!W.runtime_ready;wh.hidden=!t;const n=W.projects_ready>0,r=!t&&n&&W.x_connected;Be.hidden=r,We.hidden=!r,Be.disabled=!1,We.disabled=t||!n,Be.classList.toggle("primary",!r),We.classList.toggle("primary",r),xh.hidden=!r,jh.hidden=!r}function Ie(t){W={...W||{},...t},Ct()}function Uh(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||(W==null?void 0:W.version)||"",latest_version:t.latest_version??null,update_available:!!t.update_available,onboarding:t.onboarding}}const ze=kh();function xl(t){var n,r,o;t.theme&&_h(t.theme),(n=t.styles)!=null&&n.variables&&bh(t.styles.variables),(o=(r=t.styles)==null?void 0:r.css)!=null&&o.fonts&&$h(t.styles.css.fonts)}ze.onhostcontextchanged=xl;ze.onerror=t=>console.error(t);ze.ontoolresult=t=>{const n=Sl(t);n&&typeof n.projects_total=="number"&&(Ie(n),n.runtime_ready?$o():n.runtime_provisioning&&bo())};async function me(t,n={}){const r=await ze.callServerTool({name:t,arguments:n});return Sl(r)}async function Jn(){Q("Refreshing…");try{const[t,n]=await Promise.all([me("project_config",{status:!0}),me("runtime",{action:"status"}).catch(()=>({}))]);Ie({...Uh(t),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},onboarding:n.onboarding||t.onboarding||(W==null?void 0:W.onboarding)}),W&&!W.runtime_ready&&n.provisioning&&bo(),Q(""),$o()}catch(t){Q("Refresh failed: "+((t==null?void 0:t.message)||t))}}async function bo(){if(!Wn){Wn=!0,he.disabled=!0,he.textContent="Installing…";try{for(;;){const t=await me("runtime",{action:"status"}).catch(()=>({}));if(_o(t.progress??null),t.onboarding&&Ie({onboarding:t.onboarding}),t.runtime_ready){Ie({runtime_ready:!0}),Q("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",Q("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{Wn=!1}}}async function Eh(){var r;if(xt)return;xt=!0,Ct();const t=Date.now(),n=1200*1e3;try{for(;;){const o=await me("runtime",{action:"status"}).catch(()=>({}));o.progress&&_o(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&&Ie(e),(r=o.onboarding)!=null&&r.complete){await Jn(),Q("Setup complete.");break}if(Date.now()-t>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{xt=!1,Ct()}}async function $o(){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){zt.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]];zt.innerHTML=o.map(([e,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${e}</div></div>`).join("")}catch(t){zt.innerHTML=`<div class="muted">Stats unavailable: ${(t==null?void 0:t.message)||t}</div>`}}function yo(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",()=>yo(Be,"Starting…",async()=>{Q("Asking Claude to run setup…");try{const t=await ze.sendMessage({role:"user",content:[{type:"text",text:"Set up social autoposter 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?Q("The host rejected the setup request — type “set up social autoposter” in the chat instead."):(Q("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),Eh())}catch(t){Q("Couldn’t start setup: "+((t==null?void 0:t.message)||t))}}));ke.addEventListener("click",()=>{Ee=!Ee,zl()});Vn.addEventListener("click",()=>{Je=!Je,zt.hidden=!Je,Vn.setAttribute("aria-expanded",String(Je)),Vn.classList.toggle("expanded",Je)});We.addEventListener("click",()=>yo(We,"Drafting…",async()=>{Q("Drafting… the draft list appears in the chat for review.");try{const t=await me("draft_cycle"),n=t.drafted??0;t.onboarding&&Ie({onboarding:t.onboarding}),Q(n?`Drafted ${n} — review them in the chat and choose which to post.`:"No drafts produced."),Jn(),$o()}catch(t){Q("Draft cycle failed: "+((t==null?void 0:t.message)||t))}}));wl.addEventListener("click",t=>{const n=t.target;n&&n.id==="btn-update"&&Dh()});async function Dh(){if(Gn)return;Gn=!0;const t=document.getElementById("btn-update");t&&(t.disabled=!0,t.textContent="Updating…"),Q("Installing the latest release… this can take a minute.");try{const n=await me("runtime",{action:"update"});n.ok?(Q(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),t&&(t.textContent="Update installed — restart to apply")):(Q("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){Q("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…",Q("Installing the runtime — this is a one-time download (~150MB+).");try{const t=await me("runtime",{action:"install"});if(t.runtime_ready){Ie({runtime_ready:!0}),Jn();return}_o(t.progress??null),bo()}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 jt=null,Kn=!1;async function Ko(){if(!Kn){Kn=!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{Kn=!1}}}function Rh(){go.hidden=!0,vo.hidden=!1,Ue.textContent="Attaching to the browser…",Ko(),jt=setInterval(Ko,450)}function ko(t=!0){jt!=null&&(clearInterval(jt),jt=null),go.hidden=!1,vo.hidden=!0,Zt.hidden=!0,Zt.removeAttribute("src"),t&&me("show_browser_to_user",{action:"stop"}).catch(()=>{})}go.addEventListener("click",Rh);vo.addEventListener("click",()=>{ko(!0),Ue.textContent=""});Go.addEventListener("click",()=>yo(Go,"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."}));ze.connect().then(()=>{const t=ze.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(),hh=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(kt).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:dh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var gh=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:qn.describe("Host application identification and version."),hostCapabilities:lh.describe("Features and capabilities provided by the host."),hostContext:kl.describe("Rich context about the host environment.")}).passthrough(),vh={target:"draft-2020-12"};async function Fo(t,n){let r=t["~standard"];if(r.jsonSchema)return r.jsonSchema[n](vh);if(r.vendor==="zod"){let{z:o}=await Ul(()=>Promise.resolve().then(()=>ip),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 _h(t){let n=document.documentElement;n.setAttribute("data-theme",t),n.style.colorScheme=t}function bh(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 Wf{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:ih,toolinputpartial:oh,toolresult:fh,toolcancelled:ah,hostcontextchanged:hh});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=Bf(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 y=c.inputSchema,w=y?await Jo(y,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(uh,(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?yl:$l;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},rh,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},Qi,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},th,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},nh,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},mh,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 Qf(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}},gh,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 tr=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 yh{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 kh(){return globalThis.__SAPS_BRIDGE__==="http"?new yh:new tr({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 V=t=>document.getElementById(t),wl=V("ver"),Be=V("btn-setup"),We=V("btn-draft"),zt=V("stats-grid"),Vn=V("stats-toggle"),Sh=V("log"),wh=V("install-card"),ke=V("setup-summary"),Il=V("onboarding-details"),Ih=V("onboarding-steps"),Bn=V("onboarding-blocker"),Bo=V("onboarding-count"),zh=V("onboarding-bar-fill"),xh=V("live-card"),jh=V("stats-card"),Wo=V("install-steps"),Pe=V("install-err"),he=V("btn-install"),go=V("btn-live"),vo=V("btn-live-stop"),Go=V("btn-live-front"),Ue=V("live-status"),Zt=V("live-img");let W=null,Wn=!1,xt=!1,Gn=!1,Ee=!1,Je=!1;function Q(t){Sh.textContent=t}function Nh(t){switch(t){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function _o(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">${Nh(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),t.error?(Pe.textContent=t.error,Pe.hidden=!1):Pe.hidden=!0}const Th={environment_checked:"Environment checked",runtime_ready:"Runtime ready",x_connected:"X connected",profile_scanned:"Profile scanned",project_ready:"Project ready",topics_seeded:"Topics seeded",draft_verified:"Draft cycle verified"};function Oh(t){switch(t){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function zl(){Il.hidden=!Ee,ke.setAttribute("aria-expanded",String(Ee)),ke.classList.toggle("expanded",Ee)}function Ph(t){if(!t||!Array.isArray(t.milestones)){ke.hidden=!0,Il.hidden=!0;return}ke.hidden=!1;const n=t.milestones.length,r=t.milestones.filter(e=>e.status==="complete").length,o=!!t.current_blocker&&!t.complete;ke.classList.toggle("complete",t.complete),ke.classList.toggle("blocked",o),Bo.hidden=t.complete,Bo.textContent=o?`${r}/${n} · needs you`:xt?`${r}/${n} · setting up…`:`${r}/${n}`,zh.style.width=n>0?`${Math.round(r/n*100)}%`:"0%",Ih.innerHTML=t.milestones.map(e=>{const i=Th[e.id]||e.id,a=e.attempts>1?` <span class="detail">${e.attempts} attempts</span>`:"";return`<li class="${e.status}"><span class="glyph">${Oh(e.status)}</span><span>${i}${a}</span></li>`}).join(""),t.current_blocker?(Bn.textContent=`Current blocker: ${t.current_blocker.message}`,Bn.hidden=!1,Ee=!0):Bn.hidden=!0,zl()}function Ct(){if(!W)return;Ph(W.onboarding),wl.innerHTML=W.update_available&&W.latest_version?`v${W.version} · <button id="btn-update" class="update-btn">Update to ${W.latest_version}</button>`:`v${W.version}`;const t=!W.runtime_ready;wh.hidden=!t;const n=W.projects_ready>0,r=!t&&n&&W.x_connected;Be.hidden=r,We.hidden=!r,Be.disabled=!1,We.disabled=t||!n,Be.classList.toggle("primary",!r),We.classList.toggle("primary",r),xh.hidden=!r,jh.hidden=!r}function Ie(t){W={...W||{},...t},Ct()}function Uh(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||(W==null?void 0:W.version)||"",latest_version:t.latest_version??null,update_available:!!t.update_available,onboarding:t.onboarding}}const ze=kh();function xl(t){var n,r,o;t.theme&&_h(t.theme),(n=t.styles)!=null&&n.variables&&bh(t.styles.variables),(o=(r=t.styles)==null?void 0:r.css)!=null&&o.fonts&&$h(t.styles.css.fonts)}ze.onhostcontextchanged=xl;ze.onerror=t=>console.error(t);ze.ontoolresult=t=>{const n=Sl(t);n&&typeof n.projects_total=="number"&&(Ie(n),n.runtime_ready?$o():n.runtime_provisioning&&bo())};async function me(t,n={}){const r=await ze.callServerTool({name:t,arguments:n});return Sl(r)}async function Jn(){Q("Refreshing…");try{const[t,n]=await Promise.all([me("project_config",{status:!0}),me("runtime",{action:"status"}).catch(()=>({}))]);Ie({...Uh(t),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},onboarding:n.onboarding||t.onboarding||(W==null?void 0:W.onboarding)}),W&&!W.runtime_ready&&n.provisioning&&bo(),Q(""),$o()}catch(t){Q("Refresh failed: "+((t==null?void 0:t.message)||t))}}async function bo(){if(!Wn){Wn=!0,he.disabled=!0,he.textContent="Installing…";try{for(;;){const t=await me("runtime",{action:"status"}).catch(()=>({}));if(_o(t.progress??null),t.onboarding&&Ie({onboarding:t.onboarding}),t.runtime_ready){Ie({runtime_ready:!0}),Q("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",Q("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{Wn=!1}}}async function Eh(){var r;if(xt)return;xt=!0,Ct();const t=Date.now(),n=1200*1e3;try{for(;;){const o=await me("runtime",{action:"status"}).catch(()=>({}));o.progress&&_o(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&&Ie(e),(r=o.onboarding)!=null&&r.complete){await Jn(),Q("Setup complete.");break}if(Date.now()-t>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{xt=!1,Ct()}}async function $o(){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){zt.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]];zt.innerHTML=o.map(([e,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${e}</div></div>`).join("")}catch(t){zt.innerHTML=`<div class="muted">Stats unavailable: ${(t==null?void 0:t.message)||t}</div>`}}function yo(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",()=>yo(Be,"Starting…",async()=>{Q("Asking Claude to run setup…");try{const t=await ze.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?Q("The host rejected the setup request — type “set up S4L” in the chat instead."):(Q("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),Eh())}catch(t){Q("Couldn’t start setup: "+((t==null?void 0:t.message)||t))}}));ke.addEventListener("click",()=>{Ee=!Ee,zl()});Vn.addEventListener("click",()=>{Je=!Je,zt.hidden=!Je,Vn.setAttribute("aria-expanded",String(Je)),Vn.classList.toggle("expanded",Je)});We.addEventListener("click",()=>yo(We,"Drafting…",async()=>{Q("Drafting… the draft list appears in the chat for review.");try{const t=await me("draft_cycle"),n=t.drafted??0;t.onboarding&&Ie({onboarding:t.onboarding}),Q(n?`Drafted ${n} — review them in the chat and choose which to post.`:"No drafts produced."),Jn(),$o()}catch(t){Q("Draft cycle failed: "+((t==null?void 0:t.message)||t))}}));wl.addEventListener("click",t=>{const n=t.target;n&&n.id==="btn-update"&&Dh()});async function Dh(){if(Gn)return;Gn=!0;const t=document.getElementById("btn-update");t&&(t.disabled=!0,t.textContent="Updating…"),Q("Installing the latest release… this can take a minute.");try{const n=await me("runtime",{action:"update"});n.ok?(Q(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),t&&(t.textContent="Update installed — restart to apply")):(Q("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){Q("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…",Q("Installing the runtime — this is a one-time download (~150MB+).");try{const t=await me("runtime",{action:"install"});if(t.runtime_ready){Ie({runtime_ready:!0}),Jn();return}_o(t.progress??null),bo()}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 jt=null,Kn=!1;async function Ko(){if(!Kn){Kn=!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{Kn=!1}}}function Rh(){go.hidden=!0,vo.hidden=!1,Ue.textContent="Attaching to the browser…",Ko(),jt=setInterval(Ko,450)}function ko(t=!0){jt!=null&&(clearInterval(jt),jt=null),go.hidden=!1,vo.hidden=!0,Zt.hidden=!0,Zt.removeAttribute("src"),t&&me("show_browser_to_user",{action:"stop"}).catch(()=>{})}go.addEventListener("click",Rh);vo.addEventListener("click",()=>{ko(!0),Ue.textContent=""});Go.addEventListener("click",()=>yo(Go,"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."}));ze.connect().then(()=>{const t=ze.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>