@m13v/s4l 1.7.0 → 1.7.1-rc.10
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/bin/cli.js +23 -0
- package/mcp/dist/index.js +69 -4
- package/mcp/dist/onboarding.js +3 -10
- package/mcp/dist/panel.html +1 -1
- package/mcp/dist/runtime.js +101 -1
- package/mcp/dist/telemetry.js +80 -3
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +5 -1
- package/mcp/menubar/s4l_card.py +18 -21
- package/mcp/menubar/s4l_menubar.py +225 -27
- package/mcp/package.json +1 -1
- package/mcp/shared/doctor.cjs +8 -0
- package/mcp/shared/onboarding-ledger.cjs +1 -0
- package/package.json +1 -1
- package/scripts/autopilot_stall_watch.py +190 -15
- package/scripts/feedback_digest.py +25 -2
- package/scripts/get-latest-staging-mcpb.sh +50 -0
- package/scripts/link_tail.py +167 -91
- package/scripts/mark_event.py +128 -0
- package/scripts/memory_snapshot.py +35 -0
- package/scripts/release-mcpb.sh +48 -0
- package/scripts/reset-test-machine.sh +85 -3
- package/scripts/scheduled_tasks_snapshot.py +107 -0
- package/scripts/sentry_digest.py +303 -0
- package/scripts/sentry_init.py +43 -15
- package/scripts/setup_twitter_auth.py +76 -38
- package/skill/sentry-digest.sh +19 -0
package/bin/cli.js
CHANGED
|
@@ -823,6 +823,29 @@ function installMcp() {
|
|
|
823
823
|
} catch (e) {
|
|
824
824
|
console.warn(' WARNING: could not stamp MCP version:', e && e.message);
|
|
825
825
|
}
|
|
826
|
+
// Auto-opt a fresh install into the staging channel when the version being
|
|
827
|
+
// installed is itself a pre-release (-rc.N) — e.g. `npx social-autoposter@
|
|
828
|
+
// X.Y.Z-rc.N init`. Without this, channel.json stays absent, which
|
|
829
|
+
// scripts/s4l_channel.py's fail-safe default reads as "stable", so the box
|
|
830
|
+
// would install this one rc and then silently stop tracking staging (never
|
|
831
|
+
// pick up the next rc). Mirrors the same one-time write in
|
|
832
|
+
// mcp/src/runtime.ts's provision() for the .mcpb (Desktop) install path.
|
|
833
|
+
// Only writes when no channel marker exists yet — never overrides an
|
|
834
|
+
// existing preference (e.g. a user who already opted back out).
|
|
835
|
+
try {
|
|
836
|
+
const pkgVersion = require('../package.json').version;
|
|
837
|
+
if (pkgVersion.includes('-rc.')) {
|
|
838
|
+
const stateDir = process.env.S4L_STATE_DIR || path.join(os.homedir(), '.social-autoposter-mcp');
|
|
839
|
+
const channelPath = path.join(stateDir, 'channel.json');
|
|
840
|
+
if (!fs.existsSync(channelPath)) {
|
|
841
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
842
|
+
fs.writeFileSync(channelPath, JSON.stringify({ channel: 'staging' }, null, 2) + '\n');
|
|
843
|
+
console.log(' pre-release install detected -> opted into the staging channel');
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
} catch (e) {
|
|
847
|
+
console.warn(' WARNING: could not set staging channel:', e && e.message);
|
|
848
|
+
}
|
|
826
849
|
console.log(' installing MCP runtime deps (npm install --omit=dev in mcp/)');
|
|
827
850
|
const npmRes = spawnSync('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], {
|
|
828
851
|
cwd: mcpDest,
|
package/mcp/dist/index.js
CHANGED
|
@@ -22,10 +22,10 @@ import fs from "node:fs";
|
|
|
22
22
|
import { repoDir, runPython, run, readPlan, writePlan, planPath, } from "./repo.js";
|
|
23
23
|
import { applySetup, resolveProject, personaReady, listManagedProjectStatus, listProjectSettings, ensureShortLinksDefault, ensurePersonaProject, findPersonaProject, REQUIRED_FIELDS, RECOMMENDED_FIELDS, configPath, normalizeStringList, } from "./setup.js";
|
|
24
24
|
import { xStatus, xConnect, xDetectSources, xScanProfile, summarizeXAuth } from "./twitterAuth.js";
|
|
25
|
-
import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, resolveChrome, ensureMenubar, menubarRunning, clearMenubarStop, ensurePipelineCurrent, ensureRuntimeProvisioned, } from "./runtime.js";
|
|
25
|
+
import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, resolveChrome, ensureMenubar, menubarRunning, clearMenubarStop, ensurePipelineCurrent, ensureRuntimeProvisioned, retryProvisionIfStalled, } from "./runtime.js";
|
|
26
26
|
import { blockOnboardingMilestone, completeOnboardingMilestone, ensureDoctorPhase, onboardingLedger, onboardingSnapshot, recordOnboardingAttempt, runDoctorPhase, } from "./onboarding.js";
|
|
27
27
|
import { VERSION, versionStatus, latestPublishedVersion } from "./version.js";
|
|
28
|
-
import { initSentry, sendHeartbeat, sendStateSnapshot, captureError, flushSentry, startLogStreaming, flushLogs, logLine } from "./telemetry.js";
|
|
28
|
+
import { initSentry, sendHeartbeat, sendStateSnapshot, captureError, captureMessage, flushSentry, startLogStreaming, flushLogs, logLine, checkVersionChange } from "./telemetry.js";
|
|
29
29
|
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, getUiCapability, } from "@modelcontextprotocol/ext-apps/server";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
31
|
import http from "node:http";
|
|
@@ -1835,6 +1835,18 @@ tool("project_config", {
|
|
|
1835
1835
|
// as expected. Once connect_x succeeds, run the full phase immediately
|
|
1836
1836
|
// to verify persistence, CDP, and the durable cookie mirror.
|
|
1837
1837
|
doctorReport = await runDoctorPhase("full");
|
|
1838
|
+
// Only mark x_verified once doctor confirms all X checks pass (session valid,
|
|
1839
|
+
// cookies persisted, CDP responding, keychain accessible). This prevents the
|
|
1840
|
+
// autopilot from starting if X connection didn't actually persist.
|
|
1841
|
+
if (doctorReport?.ok) {
|
|
1842
|
+
completeOnboardingMilestone("x_verified", {
|
|
1843
|
+
x_session: doctorReport.checks?.find(c => c.id === "x_session")?.status,
|
|
1844
|
+
x_cookies: doctorReport.checks?.find(c => c.id === "x_cookie_sqlite")?.status,
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
else {
|
|
1848
|
+
recordOnboardingAttempt("x_verified", { doctor_ok: false });
|
|
1849
|
+
}
|
|
1838
1850
|
}
|
|
1839
1851
|
else {
|
|
1840
1852
|
blockOnboardingMilestone("x_connected", `x_${r.state || "not_connected"}`, r.error || r.note || summarizeXAuth(r), { state: r.state || "not_connected" });
|
|
@@ -2453,12 +2465,21 @@ tool("runtime", {
|
|
|
2453
2465
|
});
|
|
2454
2466
|
}
|
|
2455
2467
|
// ---- status (default): runtime install snapshot -----------------------
|
|
2456
|
-
|
|
2468
|
+
let snapshot = runtimeSnapshot();
|
|
2457
2469
|
if (snapshot.runtime_ready) {
|
|
2458
2470
|
completeOnboardingMilestone("runtime_ready");
|
|
2459
2471
|
}
|
|
2460
2472
|
else if (snapshot.progress?.done && !snapshot.progress.ok) {
|
|
2461
|
-
|
|
2473
|
+
// A prior provision failed. Kick a bounded auto-retry so status polling
|
|
2474
|
+
// self-heals a transient failure instead of parking until the next boot;
|
|
2475
|
+
// the provisioner cleans its own partial artifacts, so a retry is safe.
|
|
2476
|
+
// Only surface the failure to the onboarding ledger once retries are spent.
|
|
2477
|
+
if (retryProvisionIfStalled()) {
|
|
2478
|
+
snapshot = runtimeSnapshot(); // reflect the restarted, in-flight run
|
|
2479
|
+
}
|
|
2480
|
+
else {
|
|
2481
|
+
blockOnboardingMilestone("runtime_ready", "runtime_install_failed", snapshot.progress.error || "Runtime installation failed", { outcome: "failed" });
|
|
2482
|
+
}
|
|
2462
2483
|
}
|
|
2463
2484
|
return jsonContent({
|
|
2464
2485
|
...snapshot,
|
|
@@ -2531,6 +2552,29 @@ tool("report_diagnosis", {
|
|
|
2531
2552
|
return jsonContent({ ok: false, detail: String(e?.message || e).slice(0, 300) });
|
|
2532
2553
|
}
|
|
2533
2554
|
});
|
|
2555
|
+
// ---- client_event: lightweight UI telemetry ping from the dashboard panel --
|
|
2556
|
+
// The panel iframe is a browser context with no Sentry SDK and no server-side
|
|
2557
|
+
// telemetry access of its own. Before this, a panel button click (e.g. "Set up
|
|
2558
|
+
// draft schedule") had NO record anywhere — report_diagnosis needs a full
|
|
2559
|
+
// markdown report from an agent turn, which doesn't fit a plain click, so the
|
|
2560
|
+
// panel's rearm button silently had zero telemetry while its menu-bar sibling
|
|
2561
|
+
// did (see s4l_menubar.py _capture_msg). Not for agent use: the panel calls
|
|
2562
|
+
// this directly via app.callServerTool, never through chat.
|
|
2563
|
+
tool("client_event", {
|
|
2564
|
+
title: "Log a lightweight client UI event",
|
|
2565
|
+
description: "Internal telemetry hook for the dashboard panel to report a UI event (e.g. a button click). " +
|
|
2566
|
+
"Not intended for the agent to call from chat.",
|
|
2567
|
+
inputSchema: {
|
|
2568
|
+
event: z.string().describe("Short event name, e.g. rearm_clicked"),
|
|
2569
|
+
surface: z.string().optional().describe("UI surface the event came from, e.g. panel"),
|
|
2570
|
+
},
|
|
2571
|
+
}, async ({ event, surface }) => {
|
|
2572
|
+
captureMessage(`S4L client event: ${event}`, {
|
|
2573
|
+
level: "info",
|
|
2574
|
+
tags: { component: "panel", event, surface: surface || "panel" },
|
|
2575
|
+
});
|
|
2576
|
+
return jsonContent({ ok: true });
|
|
2577
|
+
});
|
|
2534
2578
|
function runtimeSnapshot() {
|
|
2535
2579
|
const rt = readRuntime();
|
|
2536
2580
|
const progress = readProgress();
|
|
@@ -3225,6 +3269,18 @@ async function ensureQueueKickerInstalled() {
|
|
|
3225
3269
|
detail: "no ready project or active persona yet",
|
|
3226
3270
|
};
|
|
3227
3271
|
}
|
|
3272
|
+
// Additional gate: X must be connected and verified before autopilot starts.
|
|
3273
|
+
// This prevents wasted cycles against a logged-out x.com if X connection didn't
|
|
3274
|
+
// persist after import/login. x_verified milestone only completes after doctor
|
|
3275
|
+
// confirms X session and cookies are valid.
|
|
3276
|
+
const onboardingState = onboardingSnapshot();
|
|
3277
|
+
const xVerified = onboardingState?.milestones?.some((m) => m.id === "x_verified" && m.status === "complete");
|
|
3278
|
+
if (!xVerified) {
|
|
3279
|
+
return {
|
|
3280
|
+
ok: false,
|
|
3281
|
+
detail: "X not yet verified (awaiting explicit pre-flight confirmation)",
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3228
3284
|
const logDir = path.join(repoDir(), "skill", "logs");
|
|
3229
3285
|
try {
|
|
3230
3286
|
fs.mkdirSync(logDir, { recursive: true });
|
|
@@ -4633,6 +4689,15 @@ async function drainApprovedBacklog() {
|
|
|
4633
4689
|
}
|
|
4634
4690
|
async function main() {
|
|
4635
4691
|
initSentry();
|
|
4692
|
+
// Detect a self-update (old_version -> new_version) as the very first thing
|
|
4693
|
+
// after Sentry is up, before anything else that could restart/exit. See
|
|
4694
|
+
// checkVersionChange's own docstring for why this exists.
|
|
4695
|
+
try {
|
|
4696
|
+
checkVersionChange();
|
|
4697
|
+
}
|
|
4698
|
+
catch (e) {
|
|
4699
|
+
console.error("[social-autoposter-mcp] version-change check failed:", e?.message || e);
|
|
4700
|
+
}
|
|
4636
4701
|
// Tee the verbatim stdout/stderr of every pipeline subprocess to the s4l
|
|
4637
4702
|
// Cloud Run relay (-> Cloud Logging) so we can troubleshoot/rescue any user
|
|
4638
4703
|
// scenario (silent stalls, partial onboarding) without asking them to ship a
|
package/mcp/dist/onboarding.js
CHANGED
|
@@ -10,18 +10,11 @@ import { createRequire } from "node:module";
|
|
|
10
10
|
import { repoDir, run } from "./repo.js";
|
|
11
11
|
import { resolvePython } from "./runtime.js";
|
|
12
12
|
const require = createRequire(import.meta.url);
|
|
13
|
-
export const ONBOARDING_MILESTONES = [
|
|
14
|
-
"environment_checked",
|
|
15
|
-
"runtime_ready",
|
|
16
|
-
"x_connected",
|
|
17
|
-
"profile_scanned",
|
|
18
|
-
"mode_chosen",
|
|
19
|
-
"project_ready",
|
|
20
|
-
"topics_seeded",
|
|
21
|
-
"tasks_scheduled",
|
|
22
|
-
];
|
|
23
13
|
const ledgerApi = require("../shared/onboarding-ledger.cjs");
|
|
24
14
|
const doctorApi = require("../shared/doctor.cjs");
|
|
15
|
+
// Re-export of the ONE runtime array (see the comment above the imports): this
|
|
16
|
+
// is not a second list, just a typed alias for ledgerApi.MILESTONES.
|
|
17
|
+
export const ONBOARDING_MILESTONES = ledgerApi.MILESTONES;
|
|
25
18
|
export function onboardingSnapshot() {
|
|
26
19
|
return ledgerApi.publicSnapshot();
|
|
27
20
|
}
|
package/mcp/dist/panel.html
CHANGED
|
@@ -70,7 +70,7 @@ Boolean requesting whether a visible border and background is provided by the ho
|
|
|
70
70
|
- omitted: host decides border`)});m({method:u("ui/request-display-mode"),params:m({mode:ot.describe("The display mode being requested.")})});var Oh=m({mode:ot.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),Th=U([u("model"),u("app")]).describe("Tool visibility scope - who can access the tool.");m({resourceUri:d().optional(),visibility:x(Th).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:Ne().optional(),permissions:Ne().optional()});m({mimeTypes:x(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:x(U([zl,xl])).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:x(zt).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:So.optional().describe("CSP configuration from resource metadata."),permissions:wo.optional().describe("Sandbox permissions from resource metadata.")})});var Ph=m({method:u("ui/notifications/tool-result"),params:Kn.describe("Standard MCP tool execution result.")}),Ul=m({toolInfo:m({id:yt.optional().describe("JSON-RPC id of the tools/call request."),tool:ko.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:gh.optional().describe("Current color theme preference."),styles:zh.optional().describe("Style configuration for theming the app."),displayMode:ot.optional().describe("How the UI is currently displayed."),availableDisplayModes:x(ot).optional().describe("Display modes the host supports."),containerDimensions:U([m({height:O().describe("Fixed container height in pixels.")}),m({maxHeight:U([O(),nt()]).optional().describe("Maximum container height in pixels.")})]).and(U([m({width:O().describe("Fixed container width in pixels.")}),m({maxWidth:U([O(),nt()]).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:H().optional().describe("Whether the device supports touch input."),hover:H().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:m({top:O().describe("Top safe area inset in pixels."),right:O().describe("Right safe area inset in pixels."),bottom:O().describe("Bottom safe area inset in pixels."),left:O().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),Uh=m({method:u("ui/notifications/host-context-changed"),params:Ul.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:x(zt).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:A(d(),F().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:Vn.describe("App identification (name and version)."),appCapabilities:Nh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var Eh=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Vn.describe("Host application identification and version."),hostCapabilities:jh.describe("Features and capabilities provided by the host."),hostContext:Ul.describe("Rich context about the host environment.")}).passthrough(),Dh={target:"draft-2020-12"};async function Qo(e,n){let r=e["~standard"];if(r.jsonSchema)return r.jsonSchema[n](Dh);if(r.vendor==="zod"){let{z:o}=await Ql(()=>Promise.resolve().then(()=>kp),void 0,import.meta.url);return o.toJSONSchema(e,{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 Yo(e,n,r=""){let o=await e["~standard"].validate(n);if(o.issues){let t=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+t)}return o.value}function Rh(e){let n=document.documentElement;n.setAttribute("data-theme",e),n.style.colorScheme=e}function Ch(e,n=document.documentElement){for(let[r,o]of Object.entries(e))o!==void 0&&n.style.setProperty(r,o)}function Zh(e){if(document.getElementById("__mcp-host-fonts"))return;let n=document.createElement("style");n.id="__mcp-host-fonts",n.textContent=e,document.head.appendChild(n)}const Ft=class Ft extends mh{constructor(r,o={},t={autoResize:!0}){super(t);C(this,"_appInfo");C(this,"_capabilities");C(this,"options");C(this,"_hostCapabilities");C(this,"_hostInfo");C(this,"_hostContext");C(this,"_registeredTools",{});C(this,"_initializedSent",!1);C(this,"eventSchemas",{toolinput:kh,toolinputpartial:Sh,toolresult:Ph,toolcancelled:wh,hostcontextchanged:Uh});C(this,"_everHadListener",new Set);C(this,"_toolHandlersInitialized",!1);C(this,"_onteardown");C(this,"_oncalltool");C(this,"_onlisttools");C(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=t,t.allowUnsafeEval||X({jitless:!0}),this.setRequestHandler(Wn,i=>(console.log("Received ping:",i.params),{})),this.setEventHandler("hostcontextchanged",void 0)}_assertInitialized(r){var t;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((t=this.options)!=null&&t.strict)throw Error(o);console.warn(`${o}. This will throw in a future release.`)}_assertHandlerTiming(r){var t;if(!Ft.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((t=this.options)!=null&&t.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=dh(this._capabilities,r)}registerTool(r,o,t){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,h)=>{if(!c.enabled)throw Error(`Tool ${r} is disabled`);let f;if(s){let _=c.inputSchema,w=_?await Yo(_,p??{},`Invalid input for tool ${r}: `):p??{};f=await t(w,h)}else f=await t(h);return c.outputSchema&&!f.isError&&(f.structuredContent=await Yo(c.outputSchema,f.structuredContent,`Invalid output for tool ${r}: `)),f}};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 t=this._registeredTools[r.name];if(!t)throw Error(`Tool ${r.name} not found`);return t.handler(r.arguments,o)},this.onlisttools=async(r,o)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([t,i])=>i.enabled).map(async([t,i])=>{let a={name:t,title:i.title,description:i.description,inputSchema:i.inputSchema?await Qo(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await Qo(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(xh,(o,t)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(o.params,t)})}get oncalltool(){return this._oncalltool}set oncalltool(r){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,r),this._oncalltool=r,this.replaceRequestHandler(Nl,(o,t)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(o.params,t)})}get onlisttools(){return this._onlisttools}set onlisttools(r){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,r),this._onlisttools=r,this.replaceRequestHandler(jl,(o,t)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(o.params,t)})}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},Kn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},Il,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},wl,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let t=r.tools?Pl:Tl;return await this.request({method:"sampling/createMessage",params:r},t,o)}sendMessage(r,o){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:r},$h,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},ao,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},bh,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},yh,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},Oh,o)}sendSizeChanged(r){return this.notification({method:"ui/notifications/size-changed",params:r})}setupSizeChangedNotifications(){let r=!1,o=0,t=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 h=Math.ceil(window.innerWidth);(h!==o||p!==t)&&(o=h,t=p,this.sendSizeChanged({width:h,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(r=new hh(window.parent,window.parent),o){var t;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:ph}},Eh,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,(t=this.options)!=null&&t.autoResize&&this.setupSizeChangedNotifications()}catch(i){throw this.close(),i}}};C(Ft,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let ur=Ft;function Xo(e){const n=e&&e.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(e&&e.content||[]).find(o=>o&&o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{}}return{}}class Ah{constructor(){C(this,"onhostcontextchanged");C(this,"onerror");C(this,"ontoolresult")}async connect(){var n,r;try{const[o,t]=await Promise.all([this.callServerTool({name:"project_config",arguments:{status:!0}}),this.callServerTool({name:"runtime",arguments:{action:"status"}})]),i=Xo(o),a=Xo(t),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 Lh(){return globalThis.__S4L_BRIDGE__==="http"?new Ah:new ur({name:"S4L Panel",version:"1.0.0"})}function El(e){const n=e.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(e.content||[]).find(o=>o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{_raw:r.text}}return{}}const R=e=>document.getElementById(e),Dl=R("ver"),Qe=R("btn-setup"),Mt=R("btn-schedule"),Ot=R("stats-grid"),Yn=R("stats-toggle"),Mh=R("log"),qh=R("install-card"),xe=R("setup-summary"),Rl=R("onboarding-details"),Hh=R("onboarding-steps"),Xn=R("onboarding-blocker"),ea=R("onboarding-count"),Fh=R("onboarding-bar-fill"),Jh=R("live-card"),Bh=R("stats-card"),ta=R("install-steps"),Re=R("install-err"),ge=R("btn-install"),Vh=R("menubar-banner"),na=R("btn-menubar-restart"),Io=R("btn-live"),zo=R("btn-live-stop"),ra=R("btn-live-front"),Ce=R("live-status"),qt=R("live-img"),Cl=R("switch-personal"),Zl=R("switch-promo"),Wh=R("split-row"),Kh=R("split-desc"),be=R("split-slider"),Gh=R("mode-sub"),Qh=R("settings-card"),er=R("settings-toggle"),fe=R("settings-body");let E=null,tr=!1,Tt=!1,nr=!1,Ze=!1,Gn=!1,Ke=!1,Ue=!1,rr=!1;function W(e){Mh.textContent=e}function Yh(e){switch(e){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function xo(e){if(!e||!Array.isArray(e.steps)){ta.innerHTML="";return}ta.innerHTML=e.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">${Yh(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),e.error?(Re.textContent=e.error,Re.hidden=!1):Re.hidden=!0}const Xh={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 eg(e){switch(e){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function Al(){Rl.hidden=!Ze,xe.setAttribute("aria-expanded",String(Ze)),xe.classList.toggle("expanded",Ze)}function Ll(e){return e?e.setup_complete!==void 0?!!e.setup_complete:!!e.runtime_ready&&(e.projects_ready||0)>0&&!!e.x_connected:!1}function tg(e){if(!e||!Array.isArray(e.milestones)){xe.hidden=!0,Rl.hidden=!0;return}xe.hidden=!1;const n=e.milestones.length,r=e.milestones.filter(i=>i.status==="complete").length,o=Ll(E),t=!!e.current_blocker&&!o;xe.classList.toggle("complete",o),xe.classList.toggle("blocked",t),ea.hidden=o,ea.textContent=t?`${r}/${n} · needs you`:Tt?`${r}/${n} · setting up…`:`${r}/${n}`,Fh.style.width=n>0?`${Math.round(r/n*100)}%`:"0%",Hh.innerHTML=e.milestones.map(i=>{const a=Xh[i.id]||i.id,s=i.attempts>1?` <span class="detail">${i.attempts} attempts</span>`:"";return`<li class="${i.status}"><span class="glyph">${eg(i.status)}</span><span>${a}${s}</span></li>`}).join(""),e.current_blocker?(Xn.textContent=`Current blocker: ${e.current_blocker.message}`,Xn.hidden=!1,Ze=!0):Xn.hidden=!0,Al()}function Ht(){if(!E)return;tg(E.onboarding),Dl.innerHTML=E.update_available&&E.latest_version?`v${E.version} · <button id="btn-update" class="update-btn">Update to ${E.latest_version}</button>`:`v${E.version}`;const e=!E.runtime_ready;qh.hidden=!e,Vh.hidden=e||E.menubar_running!==!1;const n=Ll(E);Qe.hidden=n,Qe.disabled=!1,Qe.classList.toggle("primary",!n);const r=n&&(E.schedule_state==="missing"||E.schedule_state==="disabled"||E.schedule_state==="stalled");Mt.hidden=!r,Mt.classList.toggle("primary",r);const o=E.flags||(E.mode==="promotion"?{personal_brand:!1,promotion:!0}:{personal_brand:!0,promotion:!1}),t=!!o.personal_brand,i=!!o.promotion;Cl.setAttribute("aria-checked",String(t)),Zl.setAttribute("aria-checked",String(i));const a=Math.round((typeof E.personal_brand_share=="number"?E.personal_brand_share:.5)*100);Wh.hidden=!(t&&i),Gn||(be.value=String(a),Fl(a)),Gh.textContent=t&&i?`Both lanes on: cycles split ${a}/${100-a} personal/promotion.`:!t&&!i?"No lane on; the cycle falls back to personal brand.":"",Jh.hidden=!n,Bh.hidden=!n,Qh.hidden=e}function ve(e){E={...E||{},...e},Ht()}function ng(e){const n=Array.isArray(e.projects)?e.projects:[];return{projects:n,projects_total:n.length,projects_ready:n.filter(r=>r.ready).length,x_connected:!!e.x_connected,x_state:e.x_state||"",x_handle:e.x_handle??null,...e.setup_complete!==void 0?{setup_complete:!!e.setup_complete}:{},version:e.mcp_version||(E==null?void 0:E.version)||"",latest_version:e.latest_version??null,update_available:!!e.update_available,mode:e.mode??(E==null?void 0:E.mode),flags:e.flags??(E==null?void 0:E.flags),personal_brand_share:e.personal_brand_share??(E==null?void 0:E.personal_brand_share),onboarding:e.onboarding}}const ke=Lh();function Ml(e){var n,r,o;e.theme&&Rh(e.theme),(n=e.styles)!=null&&n.variables&&Ch(e.styles.variables),(o=(r=e.styles)==null?void 0:r.css)!=null&&o.fonts&&Zh(e.styles.css.fonts)}ke.onhostcontextchanged=Ml;ke.onerror=e=>console.error(e);ke.ontoolresult=e=>{const n=El(e);n&&typeof n.projects_total=="number"&&(ve(n),n.runtime_ready?ql():n.runtime_provisioning&&jo())};async function re(e,n={}){const r=await ke.callServerTool({name:e,arguments:n});return El(r)}async function Se(){W("Refreshing…");try{const[e,n]=await Promise.all([re("project_config",{status:!0}),re("runtime",{action:"status"}).catch(()=>({}))]);ve({...ng(e),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},...typeof n.menubar_running=="boolean"?{menubar_running:n.menubar_running}:{},onboarding:n.onboarding||e.onboarding||(E==null?void 0:E.onboarding)}),E&&!E.runtime_ready&&n.provisioning&&jo(),W(""),ql()}catch(e){W("Refresh failed: "+((e==null?void 0:e.message)||e))}}async function jo(){if(!tr){tr=!0,ge.disabled=!0,ge.textContent="Installing…";try{for(;;){const e=await re("runtime",{action:"status"}).catch(()=>({}));if(xo(e.progress??null),e.onboarding&&ve({onboarding:e.onboarding}),e.runtime_ready){ve({runtime_ready:!0}),W("Runtime installed; you're ready to set up."),Se();return}const n=e.progress??null;if(n&&n.done&&!n.ok){ge.disabled=!1,ge.textContent="Retry install",W("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{tr=!1}}}async function rg(){var r;if(Tt)return;Tt=!0,Ht();const e=Date.now(),n=1200*1e3;try{for(;;){const o=await re("runtime",{action:"status"}).catch(()=>({}));o.progress&&xo(o.progress);const t={};if(typeof o.runtime_ready=="boolean"&&(t.runtime_ready=o.runtime_ready),o.onboarding&&(t.onboarding=o.onboarding),Object.keys(t).length&&ve(t),(r=o.onboarding)!=null&&r.complete){await Se(),W("Setup complete.");break}if(Date.now()-e>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{Tt=!1,Ht()}}async function ql(){try{const e=await re("get_stats",{days:7}),n=Array.isArray(e.projects)?e.projects[0]:null,r=n==null?void 0:n.posts;if(!r){Ot.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]];Ot.innerHTML=o.map(([t,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${t}</div></div>`).join("")}catch(e){Ot.innerHTML=`<div class="muted">Stats unavailable: ${(e==null?void 0:e.message)||e}</div>`}}function Qn(e,n,r){const o=e.textContent;e.disabled=!0,e.textContent=n,r().finally(()=>{e.disabled=!1,e.textContent=o,Ht()})}Qe.addEventListener("click",()=>Qn(Qe,"Starting…",async()=>{W("Asking Claude to run setup…");try{const e=await ke.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."}]});e!=null&&e.isError?W("The host rejected the setup request — type “set up S4L” in the chat instead."):(W("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),rg())}catch(e){W("Couldn’t start setup: "+((e==null?void 0:e.message)||e))}}));Mt.addEventListener("click",()=>Qn(Mt,"Setting up…",async()=>{W("Asking Claude to schedule the draft tasks for this account…");try{const e=await ke.sendMessage({role:"user",content:[{type:"text",text:'Set up the S4L draft autopilot schedule for this Claude account. If queue_setup is available, call it; then for s4l-worker call the host tool create_scheduled_task with taskId, cronExpression "* * * * *", notifyOnCompletion false (REQUIRED — the default true pops a notification every minute), and the prompt — read it from ~/.claude/scheduled-tasks/s4l-worker/SKILL.md (already on disk). If the task already exists, call update_scheduled_task with taskId s4l-worker and notifyOnCompletion false instead. Do NOT redo my X connection or project setup. Keep replies to me very short.'}]});e!=null&&e.isError?W("The host rejected it — type “set up the draft schedule” in the chat instead."):W("Scheduling is running in the chat. The draft tasks will register for this account.")}catch(e){W("Couldn’t start scheduling: "+((e==null?void 0:e.message)||e))}}));function Hl(e,n){e.addEventListener("click",async()=>{if(!e.disabled){e.disabled=!0,e.setAttribute("aria-checked",String(e.getAttribute("aria-checked")!=="true"));try{const r=await re("engagement_mode",{action:"toggle",lane:n});r&&r.flags&&ve({flags:r.flags}),await Se()}catch(r){W("Couldn’t switch lane: "+((r==null?void 0:r.message)||r)),await Se()}finally{e.disabled=!1}}})}Hl(Cl,"personal_brand");Hl(Zl,"promotion");function Fl(e){Kh.textContent=`${e}% personal brand / ${100-e}% promotion`}be.addEventListener("pointerdown",()=>{Gn=!0});be.addEventListener("input",()=>{Gn=!0,Fl(Number(be.value))});be.addEventListener("change",async()=>{const e=Number(be.value);be.disabled=!0;try{const n=await re("engagement_mode",{action:"split",split:e});n&&typeof n.personal_brand_share=="number"&&ve({personal_brand_share:n.personal_brand_share}),await Se()}catch(n){W("Couldn’t set the lane split: "+((n==null?void 0:n.message)||n)),await Se()}finally{be.disabled=!1,Gn=!1}});xe.addEventListener("click",()=>{Ze=!Ze,Al()});Yn.addEventListener("click",()=>{Ke=!Ke,Ot.hidden=!Ke,Yn.setAttribute("aria-expanded",String(Ke)),Yn.classList.toggle("expanded",Ke)});Dl.addEventListener("click",e=>{const n=e.target;n&&n.id==="btn-update"&&ig()});async function ig(){if(nr)return;nr=!0;const e=document.getElementById("btn-update");e&&(e.disabled=!0,e.textContent="Updating…"),W("Installing the latest release… this can take a minute.");try{const n=await re("runtime",{action:"update"});n.ok?(W(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),e&&(e.textContent="Update installed — restart to apply")):(W("Update failed (exit "+(n.exit_code??"?")+"). Try `npx social-autoposter@latest update` in a terminal."),e&&(e.disabled=!1,e.textContent="Retry update"))}catch(n){W("Update failed: "+((n==null?void 0:n.message)||n)),e&&(e.disabled=!1,e.textContent="Retry update")}finally{nr=!1}}ge.addEventListener("click",async()=>{Re.hidden=!0,ge.disabled=!0,ge.textContent="Starting…",W("Installing the runtime — this is a one-time download (~150MB+).");try{const e=await re("runtime",{action:"install"});if(e.runtime_ready){ve({runtime_ready:!0}),Se();return}xo(e.progress??null),jo()}catch(e){ge.disabled=!1,ge.textContent="Retry install",Re.textContent="Couldn't start install: "+((e==null?void 0:e.message)||e),Re.hidden=!1}});na.addEventListener("click",()=>Qn(na,"Restarting…",async()=>{W("Restarting the S4L menu bar…");try{const e=await re("restart_menubar");typeof e.menubar_running=="boolean"&&ve({menubar_running:e.menubar_running}),W(e.menubar_running?"Menu bar restarted.":"Couldn’t confirm the menu bar came back"+(e.detail?": "+e.detail:"."))}catch(e){W("Couldn’t restart the menu bar: "+((e==null?void 0:e.message)||e))}}));const og=["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"],ag=["description","voice","search_topics","content_angle","content_guardrails"],lr={website:"Website",description:"What it does",icp:"Target audience",voice:"Voice",differentiator:"Differentiator",search_topics:"Search topics",get_started_link:"Get-started link",content_guardrails:"Content guardrails",content_angle:"Content angle"},ia=new Set(["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"]),sg=new Set(["website","get_started_link"]);function Ye(e){const n=e.replace(/[_-]+/g," ").trim();return n?n.charAt(0).toUpperCase()+n.slice(1):e}function Jl(e){return!!e&&typeof e=="object"&&!Array.isArray(e)}function oa(e){return Array.isArray(e)&&e.every(n=>typeof n=="string")?{kind:"list",text:e.join(`
|
|
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:H().optional().describe("Whether the device supports touch input."),hover:H().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:m({top:O().describe("Top safe area inset in pixels."),right:O().describe("Right safe area inset in pixels."),bottom:O().describe("Bottom safe area inset in pixels."),left:O().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),Uh=m({method:u("ui/notifications/host-context-changed"),params:Ul.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:x(zt).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:A(d(),F().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:Vn.describe("App identification (name and version)."),appCapabilities:Nh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var Eh=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Vn.describe("Host application identification and version."),hostCapabilities:jh.describe("Features and capabilities provided by the host."),hostContext:Ul.describe("Rich context about the host environment.")}).passthrough(),Dh={target:"draft-2020-12"};async function Qo(e,n){let r=e["~standard"];if(r.jsonSchema)return r.jsonSchema[n](Dh);if(r.vendor==="zod"){let{z:o}=await Ql(()=>Promise.resolve().then(()=>kp),void 0,import.meta.url);return o.toJSONSchema(e,{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 Yo(e,n,r=""){let o=await e["~standard"].validate(n);if(o.issues){let t=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+t)}return o.value}function Rh(e){let n=document.documentElement;n.setAttribute("data-theme",e),n.style.colorScheme=e}function Ch(e,n=document.documentElement){for(let[r,o]of Object.entries(e))o!==void 0&&n.style.setProperty(r,o)}function Zh(e){if(document.getElementById("__mcp-host-fonts"))return;let n=document.createElement("style");n.id="__mcp-host-fonts",n.textContent=e,document.head.appendChild(n)}const Ft=class Ft extends mh{constructor(r,o={},t={autoResize:!0}){super(t);C(this,"_appInfo");C(this,"_capabilities");C(this,"options");C(this,"_hostCapabilities");C(this,"_hostInfo");C(this,"_hostContext");C(this,"_registeredTools",{});C(this,"_initializedSent",!1);C(this,"eventSchemas",{toolinput:kh,toolinputpartial:Sh,toolresult:Ph,toolcancelled:wh,hostcontextchanged:Uh});C(this,"_everHadListener",new Set);C(this,"_toolHandlersInitialized",!1);C(this,"_onteardown");C(this,"_oncalltool");C(this,"_onlisttools");C(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=t,t.allowUnsafeEval||X({jitless:!0}),this.setRequestHandler(Wn,i=>(console.log("Received ping:",i.params),{})),this.setEventHandler("hostcontextchanged",void 0)}_assertInitialized(r){var t;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((t=this.options)!=null&&t.strict)throw Error(o);console.warn(`${o}. This will throw in a future release.`)}_assertHandlerTiming(r){var t;if(!Ft.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((t=this.options)!=null&&t.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=dh(this._capabilities,r)}registerTool(r,o,t){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,h)=>{if(!c.enabled)throw Error(`Tool ${r} is disabled`);let f;if(s){let _=c.inputSchema,w=_?await Yo(_,p??{},`Invalid input for tool ${r}: `):p??{};f=await t(w,h)}else f=await t(h);return c.outputSchema&&!f.isError&&(f.structuredContent=await Yo(c.outputSchema,f.structuredContent,`Invalid output for tool ${r}: `)),f}};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 t=this._registeredTools[r.name];if(!t)throw Error(`Tool ${r.name} not found`);return t.handler(r.arguments,o)},this.onlisttools=async(r,o)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([t,i])=>i.enabled).map(async([t,i])=>{let a={name:t,title:i.title,description:i.description,inputSchema:i.inputSchema?await Qo(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await Qo(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(xh,(o,t)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(o.params,t)})}get oncalltool(){return this._oncalltool}set oncalltool(r){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,r),this._oncalltool=r,this.replaceRequestHandler(Nl,(o,t)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(o.params,t)})}get onlisttools(){return this._onlisttools}set onlisttools(r){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,r),this._onlisttools=r,this.replaceRequestHandler(jl,(o,t)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(o.params,t)})}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},Kn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},Il,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},wl,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let t=r.tools?Pl:Tl;return await this.request({method:"sampling/createMessage",params:r},t,o)}sendMessage(r,o){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:r},$h,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},ao,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},bh,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},yh,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},Oh,o)}sendSizeChanged(r){return this.notification({method:"ui/notifications/size-changed",params:r})}setupSizeChangedNotifications(){let r=!1,o=0,t=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 h=Math.ceil(window.innerWidth);(h!==o||p!==t)&&(o=h,t=p,this.sendSizeChanged({width:h,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(r=new hh(window.parent,window.parent),o){var t;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:ph}},Eh,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,(t=this.options)!=null&&t.autoResize&&this.setupSizeChangedNotifications()}catch(i){throw this.close(),i}}};C(Ft,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let ur=Ft;function Xo(e){const n=e&&e.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(e&&e.content||[]).find(o=>o&&o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{}}return{}}class Ah{constructor(){C(this,"onhostcontextchanged");C(this,"onerror");C(this,"ontoolresult")}async connect(){var n,r;try{const[o,t]=await Promise.all([this.callServerTool({name:"project_config",arguments:{status:!0}}),this.callServerTool({name:"runtime",arguments:{action:"status"}})]),i=Xo(o),a=Xo(t),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 Lh(){return globalThis.__S4L_BRIDGE__==="http"?new Ah:new ur({name:"S4L Panel",version:"1.0.0"})}function El(e){const n=e.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(e.content||[]).find(o=>o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{_raw:r.text}}return{}}const R=e=>document.getElementById(e),Dl=R("ver"),Qe=R("btn-setup"),Mt=R("btn-schedule"),Ot=R("stats-grid"),Yn=R("stats-toggle"),Mh=R("log"),qh=R("install-card"),xe=R("setup-summary"),Rl=R("onboarding-details"),Hh=R("onboarding-steps"),Xn=R("onboarding-blocker"),ea=R("onboarding-count"),Fh=R("onboarding-bar-fill"),Jh=R("live-card"),Bh=R("stats-card"),ta=R("install-steps"),Re=R("install-err"),ge=R("btn-install"),Vh=R("menubar-banner"),na=R("btn-menubar-restart"),Io=R("btn-live"),zo=R("btn-live-stop"),ra=R("btn-live-front"),Ce=R("live-status"),qt=R("live-img"),Cl=R("switch-personal"),Zl=R("switch-promo"),Wh=R("split-row"),Kh=R("split-desc"),be=R("split-slider"),Gh=R("mode-sub"),Qh=R("settings-card"),er=R("settings-toggle"),fe=R("settings-body");let E=null,tr=!1,Tt=!1,nr=!1,Ze=!1,Gn=!1,Ke=!1,Ue=!1,rr=!1;function W(e){Mh.textContent=e}function Yh(e){switch(e){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function xo(e){if(!e||!Array.isArray(e.steps)){ta.innerHTML="";return}ta.innerHTML=e.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">${Yh(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),e.error?(Re.textContent=e.error,Re.hidden=!1):Re.hidden=!0}const Xh={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 eg(e){switch(e){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function Al(){Rl.hidden=!Ze,xe.setAttribute("aria-expanded",String(Ze)),xe.classList.toggle("expanded",Ze)}function Ll(e){return e?e.setup_complete!==void 0?!!e.setup_complete:!!e.runtime_ready&&(e.projects_ready||0)>0&&!!e.x_connected:!1}function tg(e){if(!e||!Array.isArray(e.milestones)){xe.hidden=!0,Rl.hidden=!0;return}xe.hidden=!1;const n=e.milestones.length,r=e.milestones.filter(i=>i.status==="complete").length,o=Ll(E),t=!!e.current_blocker&&!o;xe.classList.toggle("complete",o),xe.classList.toggle("blocked",t),ea.hidden=o,ea.textContent=t?`${r}/${n} · needs you`:Tt?`${r}/${n} · setting up…`:`${r}/${n}`,Fh.style.width=n>0?`${Math.round(r/n*100)}%`:"0%",Hh.innerHTML=e.milestones.map(i=>{const a=Xh[i.id]||i.id,s=i.attempts>1?` <span class="detail">${i.attempts} attempts</span>`:"";return`<li class="${i.status}"><span class="glyph">${eg(i.status)}</span><span>${a}${s}</span></li>`}).join(""),e.current_blocker?(Xn.textContent=`Current blocker: ${e.current_blocker.message}`,Xn.hidden=!1,Ze=!0):Xn.hidden=!0,Al()}function Ht(){if(!E)return;tg(E.onboarding),Dl.innerHTML=E.update_available&&E.latest_version?`v${E.version} · <button id="btn-update" class="update-btn">Update to ${E.latest_version}</button>`:`v${E.version}`;const e=!E.runtime_ready;qh.hidden=!e,Vh.hidden=e||E.menubar_running!==!1;const n=Ll(E);Qe.hidden=n,Qe.disabled=!1,Qe.classList.toggle("primary",!n);const r=n&&(E.schedule_state==="missing"||E.schedule_state==="disabled"||E.schedule_state==="stalled");Mt.hidden=!r,Mt.classList.toggle("primary",r);const o=E.flags||(E.mode==="promotion"?{personal_brand:!1,promotion:!0}:{personal_brand:!0,promotion:!1}),t=!!o.personal_brand,i=!!o.promotion;Cl.setAttribute("aria-checked",String(t)),Zl.setAttribute("aria-checked",String(i));const a=Math.round((typeof E.personal_brand_share=="number"?E.personal_brand_share:.5)*100);Wh.hidden=!(t&&i),Gn||(be.value=String(a),Fl(a)),Gh.textContent=t&&i?`Both lanes on: cycles split ${a}/${100-a} personal/promotion.`:!t&&!i?"No lane on; the cycle falls back to personal brand.":"",Jh.hidden=!n,Bh.hidden=!n,Qh.hidden=e}function ve(e){E={...E||{},...e},Ht()}function ng(e){const n=Array.isArray(e.projects)?e.projects:[];return{projects:n,projects_total:n.length,projects_ready:n.filter(r=>r.ready).length,x_connected:!!e.x_connected,x_state:e.x_state||"",x_handle:e.x_handle??null,...e.setup_complete!==void 0?{setup_complete:!!e.setup_complete}:{},version:e.mcp_version||(E==null?void 0:E.version)||"",latest_version:e.latest_version??null,update_available:!!e.update_available,mode:e.mode??(E==null?void 0:E.mode),flags:e.flags??(E==null?void 0:E.flags),personal_brand_share:e.personal_brand_share??(E==null?void 0:E.personal_brand_share),onboarding:e.onboarding}}const ke=Lh();function Ml(e){var n,r,o;e.theme&&Rh(e.theme),(n=e.styles)!=null&&n.variables&&Ch(e.styles.variables),(o=(r=e.styles)==null?void 0:r.css)!=null&&o.fonts&&Zh(e.styles.css.fonts)}ke.onhostcontextchanged=Ml;ke.onerror=e=>console.error(e);ke.ontoolresult=e=>{const n=El(e);n&&typeof n.projects_total=="number"&&(ve(n),n.runtime_ready?ql():n.runtime_provisioning&&jo())};async function re(e,n={}){const r=await ke.callServerTool({name:e,arguments:n});return El(r)}async function Se(){W("Refreshing…");try{const[e,n]=await Promise.all([re("project_config",{status:!0}),re("runtime",{action:"status"}).catch(()=>({}))]);ve({...ng(e),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},...typeof n.menubar_running=="boolean"?{menubar_running:n.menubar_running}:{},onboarding:n.onboarding||e.onboarding||(E==null?void 0:E.onboarding)}),E&&!E.runtime_ready&&n.provisioning&&jo(),W(""),ql()}catch(e){W("Refresh failed: "+((e==null?void 0:e.message)||e))}}async function jo(){if(!tr){tr=!0,ge.disabled=!0,ge.textContent="Installing…";try{for(;;){const e=await re("runtime",{action:"status"}).catch(()=>({}));if(xo(e.progress??null),e.onboarding&&ve({onboarding:e.onboarding}),e.runtime_ready){ve({runtime_ready:!0}),W("Runtime installed; you're ready to set up."),Se();return}const n=e.progress??null;if(n&&n.done&&!n.ok){ge.disabled=!1,ge.textContent="Retry install",W("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{tr=!1}}}async function rg(){var r;if(Tt)return;Tt=!0,Ht();const e=Date.now(),n=1200*1e3;try{for(;;){const o=await re("runtime",{action:"status"}).catch(()=>({}));o.progress&&xo(o.progress);const t={};if(typeof o.runtime_ready=="boolean"&&(t.runtime_ready=o.runtime_ready),o.onboarding&&(t.onboarding=o.onboarding),Object.keys(t).length&&ve(t),(r=o.onboarding)!=null&&r.complete){await Se(),W("Setup complete.");break}if(Date.now()-e>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{Tt=!1,Ht()}}async function ql(){try{const e=await re("get_stats",{days:7}),n=Array.isArray(e.projects)?e.projects[0]:null,r=n==null?void 0:n.posts;if(!r){Ot.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]];Ot.innerHTML=o.map(([t,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${t}</div></div>`).join("")}catch(e){Ot.innerHTML=`<div class="muted">Stats unavailable: ${(e==null?void 0:e.message)||e}</div>`}}function Qn(e,n,r){const o=e.textContent;e.disabled=!0,e.textContent=n,r().finally(()=>{e.disabled=!1,e.textContent=o,Ht()})}Qe.addEventListener("click",()=>Qn(Qe,"Starting…",async()=>{W("Asking Claude to run setup…");try{const e=await ke.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."}]});e!=null&&e.isError?W("The host rejected the setup request — type “set up S4L” in the chat instead."):(W("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),rg())}catch(e){W("Couldn’t start setup: "+((e==null?void 0:e.message)||e))}}));Mt.addEventListener("click",()=>Qn(Mt,"Setting up…",async()=>{W("Asking Claude to schedule the draft tasks for this account…"),re("client_event",{event:"rearm_clicked",surface:"panel"}).catch(()=>{});try{const e=await ke.sendMessage({role:"user",content:[{type:"text",text:`Set up the S4L draft autopilot schedule for this Claude account. If queue_setup is available, call it; then for s4l-worker call the host tool create_scheduled_task with taskId, cronExpression "* * * * *", notifyOnCompletion false (REQUIRED — the default true pops a notification every minute), and the prompt — read it from ~/.claude/scheduled-tasks/s4l-worker/SKILL.md (already on disk). If the task already exists, call update_scheduled_task with taskId s4l-worker and notifyOnCompletion false instead. Before calling create_scheduled_task/list_scheduled_tasks/update_scheduled_task, run (via Bash, best-effort, ignore any error and continue if the path doesn't exist): python3 ~/.social-autoposter-mcp/repo/package/scripts/mark_event.py start <that tool's name>; immediately after it returns, run the same with 'end' instead of 'start'. Do NOT redo my X connection or project setup. Keep replies to me very short.`}]});e!=null&&e.isError?W("The host rejected it — type “set up the draft schedule” in the chat instead."):W("Scheduling is running in the chat. The draft tasks will register for this account.")}catch(e){W("Couldn’t start scheduling: "+((e==null?void 0:e.message)||e))}}));function Hl(e,n){e.addEventListener("click",async()=>{if(!e.disabled){e.disabled=!0,e.setAttribute("aria-checked",String(e.getAttribute("aria-checked")!=="true"));try{const r=await re("engagement_mode",{action:"toggle",lane:n});r&&r.flags&&ve({flags:r.flags}),await Se()}catch(r){W("Couldn’t switch lane: "+((r==null?void 0:r.message)||r)),await Se()}finally{e.disabled=!1}}})}Hl(Cl,"personal_brand");Hl(Zl,"promotion");function Fl(e){Kh.textContent=`${e}% personal brand / ${100-e}% promotion`}be.addEventListener("pointerdown",()=>{Gn=!0});be.addEventListener("input",()=>{Gn=!0,Fl(Number(be.value))});be.addEventListener("change",async()=>{const e=Number(be.value);be.disabled=!0;try{const n=await re("engagement_mode",{action:"split",split:e});n&&typeof n.personal_brand_share=="number"&&ve({personal_brand_share:n.personal_brand_share}),await Se()}catch(n){W("Couldn’t set the lane split: "+((n==null?void 0:n.message)||n)),await Se()}finally{be.disabled=!1,Gn=!1}});xe.addEventListener("click",()=>{Ze=!Ze,Al()});Yn.addEventListener("click",()=>{Ke=!Ke,Ot.hidden=!Ke,Yn.setAttribute("aria-expanded",String(Ke)),Yn.classList.toggle("expanded",Ke)});Dl.addEventListener("click",e=>{const n=e.target;n&&n.id==="btn-update"&&ig()});async function ig(){if(nr)return;nr=!0;const e=document.getElementById("btn-update");e&&(e.disabled=!0,e.textContent="Updating…"),W("Installing the latest release… this can take a minute.");try{const n=await re("runtime",{action:"update"});n.ok?(W(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),e&&(e.textContent="Update installed — restart to apply")):(W("Update failed (exit "+(n.exit_code??"?")+"). Try `npx social-autoposter@latest update` in a terminal."),e&&(e.disabled=!1,e.textContent="Retry update"))}catch(n){W("Update failed: "+((n==null?void 0:n.message)||n)),e&&(e.disabled=!1,e.textContent="Retry update")}finally{nr=!1}}ge.addEventListener("click",async()=>{Re.hidden=!0,ge.disabled=!0,ge.textContent="Starting…",W("Installing the runtime — this is a one-time download (~150MB+).");try{const e=await re("runtime",{action:"install"});if(e.runtime_ready){ve({runtime_ready:!0}),Se();return}xo(e.progress??null),jo()}catch(e){ge.disabled=!1,ge.textContent="Retry install",Re.textContent="Couldn't start install: "+((e==null?void 0:e.message)||e),Re.hidden=!1}});na.addEventListener("click",()=>Qn(na,"Restarting…",async()=>{W("Restarting the S4L menu bar…");try{const e=await re("restart_menubar");typeof e.menubar_running=="boolean"&&ve({menubar_running:e.menubar_running}),W(e.menubar_running?"Menu bar restarted.":"Couldn’t confirm the menu bar came back"+(e.detail?": "+e.detail:"."))}catch(e){W("Couldn’t restart the menu bar: "+((e==null?void 0:e.message)||e))}}));const og=["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"],ag=["description","voice","search_topics","content_angle","content_guardrails"],lr={website:"Website",description:"What it does",icp:"Target audience",voice:"Voice",differentiator:"Differentiator",search_topics:"Search topics",get_started_link:"Get-started link",content_guardrails:"Content guardrails",content_angle:"Content angle"},ia=new Set(["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"]),sg=new Set(["website","get_started_link"]);function Ye(e){const n=e.replace(/[_-]+/g," ").trim();return n?n.charAt(0).toUpperCase()+n.slice(1):e}function Jl(e){return!!e&&typeof e=="object"&&!Array.isArray(e)}function oa(e){return Array.isArray(e)&&e.every(n=>typeof n=="string")?{kind:"list",text:e.join(`
|
|
74
74
|
`)}:e==null||typeof e=="string"?{kind:"text",text:String(e??"")}:{kind:"json",text:JSON.stringify(e,null,2)}}function aa(e,n,r){const o=document.createElement("div");o.className="settings-field";const t=document.createElement("label");t.textContent=e+(n.kind==="list"?" (one per line)":""),o.appendChild(t);let i;if(n.kind==="text"&&r)i=document.createElement("input"),i.type="text",i.className="settings-input";else{const a=document.createElement("textarea"),s=n.text?n.text.split(`
|
|
75
75
|
`).reduce((c,p)=>c+Math.max(1,Math.ceil(p.length/60)),0):1;a.rows=Math.min(10,Math.max(2,s)),a.className="settings-textarea"+(n.kind==="json"?" mono":""),i=a}return i.value=n.text,n.text||(i.placeholder="Not set"),i.dataset.orig=n.text,o.appendChild(i),{wrap:o,el:i}}function cg(e){const n=document.createElement("div");n.className="settings-project";const r=document.createElement("div");r.className="settings-project-head";const o=document.createElement("span");if(o.className="settings-project-name",o.textContent=e.name,r.appendChild(o),e.persona){const f=document.createElement("span");f.className="settings-project-tag",f.textContent="personal brand",r.appendChild(f)}const t=document.createElement("span");t.className="settings-project-state",t.textContent=e.ready?"ready":"missing: "+e.missing_required.join(", "),r.appendChild(t),n.appendChild(r);const i=[],a=e.persona?ag:og;for(const f of a){const _=e.fields[f];if(Jl(_)&&Object.keys(_).length){const z=document.createElement("fieldset");z.className="settings-group";const S=document.createElement("legend");S.textContent=lr[f]||Ye(f),z.appendChild(S);for(const[g,$]of Object.entries(_)){const y=oa($),I=aa(Ye(g),y,!1);z.appendChild(I.wrap),i.push({key:f,sub:g,kind:y.kind,el:I.el,orig:y.text})}n.appendChild(z);continue}const w=f==="search_topics"?{kind:"list",text:Array.isArray(_)?_.map(String).join(`
|
|
76
76
|
`):String(_??"")}:oa(_),b=aa(lr[f]||Ye(f),w,sg.has(f));n.appendChild(b.wrap),i.push({key:f,kind:w.kind,el:b.el,orig:w.text})}if(e.extra_keys.length){const f=document.createElement("div");f.className="settings-extra",f.textContent="Advanced (edit via chat): "+e.extra_keys.join(", "),n.appendChild(f)}const s=document.createElement("div");s.className="settings-actions";const c=document.createElement("button");c.className="primary",c.textContent="Save changes",c.disabled=!0;const p=document.createElement("span");p.className="settings-status",s.appendChild(c),s.appendChild(p),n.appendChild(s);const h=()=>{c.disabled=!i.some(f=>f.el.value!==f.orig)};for(const f of i)f.el.addEventListener("input",h);return c.addEventListener("click",()=>void ug(e,i,c,p)),n}async function ug(e,n,r,o){const t=n.filter(h=>h.el.value!==h.orig);if(!t.length)return;const i={name:e.name},a={},s=h=>(lr[h.key]||Ye(h.key))+(h.sub?` · ${Ye(h.sub)}`:""),c=[...new Set(t.filter(h=>h.sub!==void 0).map(h=>h.key))];for(const h of c){const f=Jl(e.fields[h])?{...e.fields[h]}:{};for(const _ of n.filter(w=>w.key===h&&w.sub!==void 0)){const w=_.el.value;if(_.kind==="list"){const b=w.split(`
|
package/mcp/dist/runtime.js
CHANGED
|
@@ -509,12 +509,57 @@ export function startProvisioning() {
|
|
|
509
509
|
if (!inFlight) {
|
|
510
510
|
const progress = freshProgress();
|
|
511
511
|
writeProgress(progress);
|
|
512
|
+
// Heartbeat: the long download steps (Chromium ~150MB, Google Chrome DMG)
|
|
513
|
+
// don't call setStep for minutes, so the progress file's updated_at froze
|
|
514
|
+
// and pollers couldn't tell "slow" from "hung." Re-stamp updated_at on a
|
|
515
|
+
// timer while the run is live so status always shows real motion.
|
|
516
|
+
const heartbeat = setInterval(() => {
|
|
517
|
+
try {
|
|
518
|
+
if (!progress.done)
|
|
519
|
+
writeProgress(progress);
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
/* best effort; never let the heartbeat throw into the timer */
|
|
523
|
+
}
|
|
524
|
+
}, 15000);
|
|
525
|
+
if (typeof heartbeat.unref === "function")
|
|
526
|
+
heartbeat.unref();
|
|
512
527
|
inFlight = provision(progress).finally(() => {
|
|
528
|
+
clearInterval(heartbeat);
|
|
513
529
|
inFlight = null;
|
|
514
530
|
});
|
|
515
531
|
}
|
|
516
532
|
return readProgress() ?? freshProgress();
|
|
517
533
|
}
|
|
534
|
+
// Bounded auto-retry for a provision that ended in failure. Called from the
|
|
535
|
+
// runtime `status` handler on each poll: if the last run failed (done && !ok)
|
|
536
|
+
// and nothing is in flight, kick a fresh run so a TRANSIENT failure (a dropped
|
|
537
|
+
// Chromium download, a flaky DMG mount) self-heals during normal status polling
|
|
538
|
+
// instead of parking until the next server boot. The venv/harness steps clean
|
|
539
|
+
// their own partial artifacts, so a retry starts from a clean slate. Capped so a
|
|
540
|
+
// genuinely broken environment (no network, no disk) surfaces the error instead
|
|
541
|
+
// of looping forever. Returns true if it started a retry.
|
|
542
|
+
let autoRetryCount = 0;
|
|
543
|
+
const MAX_AUTO_RETRIES = 3;
|
|
544
|
+
export function retryProvisionIfStalled() {
|
|
545
|
+
try {
|
|
546
|
+
if (runtimeReady())
|
|
547
|
+
return false;
|
|
548
|
+
if (isProvisioning())
|
|
549
|
+
return false;
|
|
550
|
+
const p = readProgress();
|
|
551
|
+
if (!(p && p.done && !p.ok))
|
|
552
|
+
return false; // only retry a real failure
|
|
553
|
+
if (autoRetryCount >= MAX_AUTO_RETRIES)
|
|
554
|
+
return false;
|
|
555
|
+
autoRetryCount += 1;
|
|
556
|
+
startProvisioning();
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
return false; // best-effort; must never break a status poll
|
|
561
|
+
}
|
|
562
|
+
}
|
|
518
563
|
// Boot-time deterministic provisioning: bring the owned runtime to ready on
|
|
519
564
|
// every server start WITHOUT relying on the agent to call `runtime
|
|
520
565
|
// action:'install'`. Called from main() on every server start, which the host
|
|
@@ -548,6 +593,9 @@ async function provision(progress) {
|
|
|
548
593
|
const setStep = (id, status, detail) => {
|
|
549
594
|
const st = progress.steps.find((s) => s.id === id);
|
|
550
595
|
if (st) {
|
|
596
|
+
if (status === "running" && st.status !== "running") {
|
|
597
|
+
st.started_at = new Date().toISOString();
|
|
598
|
+
}
|
|
551
599
|
st.status = status;
|
|
552
600
|
if (detail !== undefined)
|
|
553
601
|
st.detail = detail;
|
|
@@ -628,6 +676,29 @@ async function provision(progress) {
|
|
|
628
676
|
/* best effort; S4L_REPO_DIR + the run-*.sh fallback also resolve the repo */
|
|
629
677
|
}
|
|
630
678
|
setStep("repo", "done", `unpacked to ${resolvedRepo}`);
|
|
679
|
+
// Auto-opt a bare .mcpb FIRST install into the staging channel when the
|
|
680
|
+
// shipped build is itself a pre-release (-rc.N) — e.g. downloaded from
|
|
681
|
+
// the staging-latest alias link (scripts/release-mcpb.sh step 7b).
|
|
682
|
+
// Without this, channel.json stays absent, which releaseChannel()'s
|
|
683
|
+
// fail-safe default reads as "stable" (mcp/src/version.ts), so the box
|
|
684
|
+
// would install this one rc and then silently stop tracking staging
|
|
685
|
+
// (never pick up the next rc). Mirrors the same one-time write in
|
|
686
|
+
// bin/cli.js's installMcp() for the npx install path. Gated to this
|
|
687
|
+
// `else` branch (genuine bare-.mcpb unpack, not envClone) so a
|
|
688
|
+
// developer's own npm/git clone is never auto-enrolled. Only writes when
|
|
689
|
+
// no channel marker exists yet — never overrides an existing preference.
|
|
690
|
+
try {
|
|
691
|
+
const bv = bundledVersion();
|
|
692
|
+
const channelPath = path.join(STATE_DIR, "channel.json");
|
|
693
|
+
if (bv && bv.includes("-rc.") && !fs.existsSync(channelPath)) {
|
|
694
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
695
|
+
fs.writeFileSync(channelPath, JSON.stringify({ channel: "staging" }, null, 2) + "\n", "utf-8");
|
|
696
|
+
console.error(`[runtime] first install of a staging build (${bv}) — opted into the staging channel`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
/* best effort; worst case the box just tracks stable */
|
|
701
|
+
}
|
|
631
702
|
}
|
|
632
703
|
// --- Step 1: uv -----------------------------------------------------------
|
|
633
704
|
setStep("uv", "running");
|
|
@@ -658,9 +729,25 @@ async function provision(progress) {
|
|
|
658
729
|
}
|
|
659
730
|
setStep("python", "done");
|
|
660
731
|
// --- Step 3: owned venv ---------------------------------------------------
|
|
732
|
+
// A prior aborted run can leave a PARTIAL .venv (dir created, but the
|
|
733
|
+
// interpreter symlink never landed because the standalone-Python download was
|
|
734
|
+
// cut off). `uv venv` refuses a non-empty target, so every retry then failed
|
|
735
|
+
// identically and the ONLY escape was a human `rm -rf .venv` — the single
|
|
736
|
+
// non-idempotent step that turned a 5-minute install into an hour. Treat a
|
|
737
|
+
// venv with no interpreter as garbage and clear it first (same defensive idiom
|
|
738
|
+
// Step 0 uses for a half-unpacked repo), and pass --clear so uv rebuilds
|
|
739
|
+
// cleanly. This makes re-provision fully self-healing.
|
|
661
740
|
setStep("venv", "running");
|
|
662
741
|
{
|
|
663
|
-
|
|
742
|
+
if (fs.existsSync(VENV_DIR) && !fs.existsSync(VENV_PYTHON)) {
|
|
743
|
+
try {
|
|
744
|
+
fs.rmSync(VENV_DIR, { recursive: true, force: true });
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
/* best effort; --clear below is the second line of defense */
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const r = await sh(uv, ["venv", "--clear", "--python", PYTHON_VERSION, VENV_DIR], {
|
|
664
751
|
env: uvEnv,
|
|
665
752
|
timeoutMs: 120000,
|
|
666
753
|
});
|
|
@@ -714,6 +801,19 @@ async function provision(progress) {
|
|
|
714
801
|
// draft_cycle returned "no candidates". This brings .mcpb to parity with npm.
|
|
715
802
|
setStep("harness", "running");
|
|
716
803
|
{
|
|
804
|
+
// Retry-safety: a clone cut off mid-way leaves HARNESS_DIR present but with
|
|
805
|
+
// no .git, so the "clone if absent" check below skips it and the fetch/reset
|
|
806
|
+
// then fail forever against a broken checkout — the same partial-artifact
|
|
807
|
+
// deadlock the venv step had. Treat a non-git HARNESS_DIR as garbage and
|
|
808
|
+
// rebuild it.
|
|
809
|
+
if (fs.existsSync(HARNESS_DIR) && !fs.existsSync(path.join(HARNESS_DIR, ".git"))) {
|
|
810
|
+
try {
|
|
811
|
+
fs.rmSync(HARNESS_DIR, { recursive: true, force: true });
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
/* best effort */
|
|
815
|
+
}
|
|
816
|
+
}
|
|
717
817
|
// Clone if absent (mkdir parent first), else reuse the checkout.
|
|
718
818
|
if (!fs.existsSync(HARNESS_DIR)) {
|
|
719
819
|
fs.mkdirSync(path.dirname(HARNESS_DIR), { recursive: true });
|
package/mcp/dist/telemetry.js
CHANGED
|
@@ -18,6 +18,9 @@ import { VERSION } from "./version.js";
|
|
|
18
18
|
const EMBEDDED_DSN = "https://4d44ac907262c6545cf8681703528d04@o4507617161314304.ingest.us.sentry.io/4511598804336640";
|
|
19
19
|
const SENTRY_DSN = process.env.S4L_SENTRY_DSN || EMBEDDED_DSN;
|
|
20
20
|
let sentryReady = false;
|
|
21
|
+
// Cached install_id (set by tagInstall), reused to fingerprint every event per
|
|
22
|
+
// install — see captureError/captureMessage below.
|
|
23
|
+
let cachedInstallId = null;
|
|
21
24
|
export function initSentry() {
|
|
22
25
|
if (sentryReady || !SENTRY_DSN)
|
|
23
26
|
return;
|
|
@@ -51,8 +54,10 @@ async function tagInstall() {
|
|
|
51
54
|
if (res.code !== 0)
|
|
52
55
|
return;
|
|
53
56
|
const id = JSON.parse(res.stdout || "{}");
|
|
54
|
-
if (id.install_id)
|
|
57
|
+
if (id.install_id) {
|
|
55
58
|
Sentry.setTag("install_id", String(id.install_id));
|
|
59
|
+
cachedInstallId = String(id.install_id);
|
|
60
|
+
}
|
|
56
61
|
if (id.hostname)
|
|
57
62
|
Sentry.setTag("hostname", String(id.hostname));
|
|
58
63
|
}
|
|
@@ -62,8 +67,41 @@ async function tagInstall() {
|
|
|
62
67
|
}
|
|
63
68
|
export function captureError(err, tags) {
|
|
64
69
|
try {
|
|
65
|
-
if (sentryReady)
|
|
66
|
-
|
|
70
|
+
if (!sentryReady)
|
|
71
|
+
return;
|
|
72
|
+
Sentry.withScope((scope) => {
|
|
73
|
+
for (const [k, v] of Object.entries(tags || {}))
|
|
74
|
+
scope.setTag(k, v);
|
|
75
|
+
// Mix install_id into the grouping key so the SAME error on two different
|
|
76
|
+
// customer boxes lands in two different issues, not one conflated issue
|
|
77
|
+
// whose "latest event" can silently belong to a different install than
|
|
78
|
+
// the one you filtered for (default grouping ignores tags entirely).
|
|
79
|
+
if (cachedInstallId)
|
|
80
|
+
scope.setFingerprint(["{{ default }}", cachedInstallId]);
|
|
81
|
+
Sentry.captureException(err);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
/* swallow */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Report a handled, non-exception CONDITION (mirrors scripts/sentry_init.py's
|
|
89
|
+
// capture_message on the Python side — added 2026-07-07 alongside it so both
|
|
90
|
+
// halves of the client have parity). Use for operational states, not thrown
|
|
91
|
+
// errors: captureError only covers exceptions.
|
|
92
|
+
export function captureMessage(message, opts) {
|
|
93
|
+
try {
|
|
94
|
+
if (!sentryReady)
|
|
95
|
+
return;
|
|
96
|
+
Sentry.withScope((scope) => {
|
|
97
|
+
for (const [k, v] of Object.entries(opts?.tags || {}))
|
|
98
|
+
scope.setTag(k, v);
|
|
99
|
+
for (const [k, v] of Object.entries(opts?.extra || {}))
|
|
100
|
+
scope.setExtra(k, v);
|
|
101
|
+
if (cachedInstallId)
|
|
102
|
+
scope.setFingerprint(["{{ default }}", cachedInstallId]);
|
|
103
|
+
Sentry.captureMessage(message, opts?.level || "info");
|
|
104
|
+
});
|
|
67
105
|
}
|
|
68
106
|
catch {
|
|
69
107
|
/* swallow */
|
|
@@ -78,6 +116,45 @@ export async function flushSentry(ms = 2000) {
|
|
|
78
116
|
/* swallow */
|
|
79
117
|
}
|
|
80
118
|
}
|
|
119
|
+
function lastVersionPath() {
|
|
120
|
+
return path.join(snapshotStateDir(), "last-version.json");
|
|
121
|
+
}
|
|
122
|
+
// Detect a version change on boot (a .mcpb self-update, or Desktop reloading
|
|
123
|
+
// the extension after one) and log ONE explicit milestone: old_version ->
|
|
124
|
+
// new_version. Before this, "was this an update?" had to be inferred after
|
|
125
|
+
// the fact by cross-referencing THREE unrelated weak signals — app_version in
|
|
126
|
+
// memory_snapshot, reason="startup" in installation_state_snapshots, and the
|
|
127
|
+
// menu bar's own boot line — across a manual full-day log sweep (the Karol
|
|
128
|
+
// 2026-07-07 update-orphan investigation). Call once, early in main(), before
|
|
129
|
+
// anything else that might restart or exit. Best-effort; never throws.
|
|
130
|
+
export function checkVersionChange() {
|
|
131
|
+
const to = VERSION;
|
|
132
|
+
let from = null;
|
|
133
|
+
try {
|
|
134
|
+
const raw = fs.readFileSync(lastVersionPath(), "utf-8");
|
|
135
|
+
from = JSON.parse(raw)?.version ?? null;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
from = null; // no cached version -> first boot ever, or file missing/corrupt
|
|
139
|
+
}
|
|
140
|
+
const isFirstBoot = from === null;
|
|
141
|
+
const changed = !isFirstBoot && from !== to;
|
|
142
|
+
try {
|
|
143
|
+
fs.mkdirSync(snapshotStateDir(), { recursive: true });
|
|
144
|
+
fs.writeFileSync(lastVersionPath(), JSON.stringify({ version: to, seen_at: new Date().toISOString() }), "utf-8");
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
/* best-effort; a failed write just means the next boot re-detects from "from: null" */
|
|
148
|
+
}
|
|
149
|
+
if (changed) {
|
|
150
|
+
console.error(`[social-autoposter-mcp] self-update detected: ${from} -> ${to}`);
|
|
151
|
+
captureMessage(`social-autoposter self-update: ${from} -> ${to}`, {
|
|
152
|
+
level: "info",
|
|
153
|
+
tags: { component: "update", issue: "self_update", from_version: from || "", to_version: to },
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return { changed, isFirstBoot, from, to };
|
|
157
|
+
}
|
|
81
158
|
// Phone home so .mcpb installs show up in the install-lane digest, parity with
|
|
82
159
|
// the npx launchd heartbeat. Best-effort; never throws.
|
|
83
160
|
export async function sendHeartbeat(reason) {
|
package/mcp/dist/version.json
CHANGED
package/mcp/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"dxt_version": "0.1",
|
|
3
3
|
"name": "social-autoposter",
|
|
4
4
|
"display_name": "S4L",
|
|
5
|
-
"version": "1.7.
|
|
5
|
+
"version": "1.7.1-rc.10",
|
|
6
6
|
"description": "Draft, review, approve, and autopilot X/Twitter posts.",
|
|
7
7
|
"long_description": "## **⚠️ The disclaimer above is generic Claude boilerplate.** Anthropic shows the same warning on every plugin regardless of what it does; any plugin has the same level of access as any app you download from the internet.\n\nS4L 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 with CMD+Q, reopen Claude, paste into a new chat.\n\nWhat happens next:\n\n* About every 5 minutes S4L scans X for posts that match your topics and drafts replies in your voice.\n* Drafts show up as review cards, usually the first within a few minutes. Nothing is posted automatically; you approve each one.\n* Posting autopilot stays off until you explicitly turn it on.",
|
|
8
8
|
"author": {
|
|
@@ -52,6 +52,10 @@
|
|
|
52
52
|
"name": "report_diagnosis",
|
|
53
53
|
"description": "Send a diagnosis report to the S4L developers"
|
|
54
54
|
},
|
|
55
|
+
{
|
|
56
|
+
"name": "client_event",
|
|
57
|
+
"description": "Log a lightweight client UI event"
|
|
58
|
+
},
|
|
55
59
|
{
|
|
56
60
|
"name": "queue_setup",
|
|
57
61
|
"description": "Get autopilot scheduled-task specs"
|