@m13v/s4l 1.7.6-rc.30 → 1.7.6-rc.32
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 +29 -28
- package/mcp/dist/panel.html +9 -9
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +5 -5
- package/mcp/menubar/s4l_menubar.py +46 -40
- package/mcp/menubar/s4l_state.py +31 -8
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/s4l_posting_mode.py +64 -5
package/mcp/dist/index.js
CHANGED
|
@@ -3367,35 +3367,36 @@ tool("restart_menubar", {
|
|
|
3367
3367
|
menubar_running: running,
|
|
3368
3368
|
});
|
|
3369
3369
|
});
|
|
3370
|
-
// ----
|
|
3371
|
-
// The
|
|
3372
|
-
//
|
|
3373
|
-
//
|
|
3374
|
-
//
|
|
3375
|
-
//
|
|
3376
|
-
//
|
|
3377
|
-
//
|
|
3378
|
-
//
|
|
3379
|
-
//
|
|
3370
|
+
// ---- run_drafting: run the drafting pipeline immediately -------------------
|
|
3371
|
+
// The drafting pipeline (the twitter cycle) is what the user triggers: it SCANS
|
|
3372
|
+
// for threads first, then hands off to the drafter, so from the user's view this
|
|
3373
|
+
// is "run drafting now". It runs on the launchd kicker's fixed StartInterval;
|
|
3374
|
+
// this nudges launchd to fire it RIGHT NOW via `launchctl kickstart` — the EXACT
|
|
3375
|
+
// same mechanism the first-run onboarding kick uses (ensureQueueKickerInstalled):
|
|
3376
|
+
// the job runs THROUGH launchd with its full baked env + the run-*-singleton.sh
|
|
3377
|
+
// lock, so it is NOT a bare `bash run-twitter-cycle.sh` (which would leave an
|
|
3378
|
+
// empty-plan artifact). launchd keeps a single instance, so if a run is already
|
|
3379
|
+
// underway this is a harmless no-op; while paused the kicker is unloaded, so we
|
|
3380
|
+
// report that rather than kick nothing.
|
|
3380
3381
|
//
|
|
3381
3382
|
// This is deliberately NOT the removed run_draft_cycle / draft_cycle tool: it
|
|
3382
3383
|
// spawns no warm in-chat Claude draft session, it only asks launchd to run the
|
|
3383
3384
|
// already-scheduled cycle one interval early. Do NOT re-introduce a draft_cycle.
|
|
3384
|
-
tool("
|
|
3385
|
-
title: "Run the
|
|
3386
|
-
description: "
|
|
3387
|
-
"
|
|
3388
|
-
"
|
|
3389
|
-
"
|
|
3390
|
-
"
|
|
3391
|
-
"
|
|
3385
|
+
tool("run_drafting", {
|
|
3386
|
+
title: "Run the drafting pipeline now",
|
|
3387
|
+
description: "Run the drafting pipeline ONE time immediately instead of waiting for the next scheduled run. " +
|
|
3388
|
+
"The pipeline scans for threads and then drafts replies, so this is the 'run now' control behind " +
|
|
3389
|
+
"the menu bar + dashboard button. It nudges the existing launchd job to run one interval early; " +
|
|
3390
|
+
"it does NOT change the schedule. If a run is already underway it's a no-op, and if S4L is paused " +
|
|
3391
|
+
"it reports that (nothing runs while paused). Use when the user asks to run drafting, draft now, " +
|
|
3392
|
+
"run the pipeline, run now, or check for new threads immediately.",
|
|
3392
3393
|
inputSchema: {},
|
|
3393
3394
|
}, async () => {
|
|
3394
3395
|
if (isPaused()) {
|
|
3395
3396
|
return jsonContent({
|
|
3396
3397
|
ok: false,
|
|
3397
3398
|
paused: true,
|
|
3398
|
-
detail: "S4L is paused — resume it first, then
|
|
3399
|
+
detail: "S4L is paused — resume it first, then drafting can run.",
|
|
3399
3400
|
});
|
|
3400
3401
|
}
|
|
3401
3402
|
const uid = process.getuid ? process.getuid() : 0;
|
|
@@ -3404,21 +3405,21 @@ tool("scan_now", {
|
|
|
3404
3405
|
return jsonContent({
|
|
3405
3406
|
ok,
|
|
3406
3407
|
detail: ok
|
|
3407
|
-
? "
|
|
3408
|
-
: `Could not start
|
|
3408
|
+
? "Drafting started — new drafts land in a few minutes."
|
|
3409
|
+
: `Could not start drafting (launchctl rc=${kick.code}).`,
|
|
3409
3410
|
});
|
|
3410
3411
|
});
|
|
3411
|
-
// ----
|
|
3412
|
-
// A cheap, pure READ the dashboard widget polls (~5s) to keep its
|
|
3412
|
+
// ---- drafting_status: live pipeline status + next-run countdown ------------
|
|
3413
|
+
// A cheap, pure READ the dashboard widget polls (~5s) to keep its drafting status
|
|
3413
3414
|
// pill + countdown live between heavier snapshot refreshes. Returns the SAME
|
|
3414
3415
|
// fields the menu bar renders (scripts/live_status.py — activity_state,
|
|
3415
3416
|
// next_run_secs, …), plus `paused`, so the two surfaces can't drift. No side
|
|
3416
3417
|
// effects: safe to poll and to replay over the loopback /tool/ endpoint.
|
|
3417
|
-
tool("
|
|
3418
|
-
title: "Live
|
|
3419
|
-
description: "Read-only: what the pipeline is doing right now (scanning/drafting/posting/idle) and
|
|
3420
|
-
"seconds until the next
|
|
3421
|
-
"read; safe to poll frequently.",
|
|
3418
|
+
tool("drafting_status", {
|
|
3419
|
+
title: "Live drafting-pipeline status + next-run countdown",
|
|
3420
|
+
description: "Read-only: what the drafting pipeline is doing right now (scanning/drafting/posting/idle) and " +
|
|
3421
|
+
"how many seconds until the next scheduled run. Backs the dashboard's live status pill + " +
|
|
3422
|
+
"countdown. Pure read; safe to poll frequently.",
|
|
3422
3423
|
inputSchema: {},
|
|
3423
3424
|
}, async () => {
|
|
3424
3425
|
let live = {};
|
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:st.describe("The display mode being requested.")})});var Ah=m({mode:st.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),Lh=D([u("model"),u("app")]).describe("Tool visibility scope - who can access the tool.");m({resourceUri:d().optional(),visibility:x(Lh).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:Te().optional(),permissions:Te().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(D([Dl,Cl])).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(jt).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:Po.optional().describe("CSP configuration from resource metadata."),permissions:Uo.optional().describe("Sandbox permissions from resource metadata.")})});var Mh=m({method:u("ui/notifications/tool-result"),params:er.describe("Standard MCP tool execution result.")}),ql=m({toolInfo:m({id:kt.optional().describe("JSON-RPC id of the tools/call request."),tool:To.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:Ih.optional().describe("Current color theme preference."),styles:Dh.optional().describe("Style configuration for theming the app."),displayMode:st.optional().describe("How the UI is currently displayed."),availableDisplayModes:x(st).optional().describe("Display modes the host supports."),containerDimensions:D([m({height:T().describe("Fixed container height in pixels.")}),m({maxHeight:D([T(),it()]).optional().describe("Maximum container height in pixels.")})]).and(D([m({width:T().describe("Fixed container width in pixels.")}),m({maxWidth:D([T(),it()]).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:D([u("web"),u("desktop"),u("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:m({touch:F().optional().describe("Whether the device supports touch input."),hover:F().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(),qh=m({method:u("ui/notifications/host-context-changed"),params:ql.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:x(jt).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:L(d(),J().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:Yn.describe("App identification (name and version)."),appCapabilities:Zh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var Hh=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Yn.describe("Host application identification and version."),hostCapabilities:Rh.describe("Features and capabilities provided by the host."),hostContext:ql.describe("Rich context about the host environment.")}).passthrough(),Fh={target:"draft-2020-12"};async function aa(e,n){let r=e["~standard"];if(r.jsonSchema)return r.jsonSchema[n](Fh);if(r.vendor==="zod"){let{z:o}=await ad(()=>Promise.resolve().then(()=>Tp),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 sa(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 Jh(e){let n=document.documentElement;n.setAttribute("data-theme",e),n.style.colorScheme=e}function Bh(e,n=document.documentElement){for(let[r,o]of Object.entries(e))o!==void 0&&n.style.setProperty(r,o)}function Vh(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 Kt=class Kt extends $h{constructor(r,o={},t={autoResize:!0}){super(t);R(this,"_appInfo");R(this,"_capabilities");R(this,"options");R(this,"_hostCapabilities");R(this,"_hostInfo");R(this,"_hostContext");R(this,"_registeredTools",{});R(this,"_initializedSent",!1);R(this,"eventSchemas",{toolinput:Th,toolinputpartial:Ph,toolresult:Mh,toolcancelled:Uh,hostcontextchanged:qh});R(this,"_everHadListener",new Set);R(this,"_toolHandlersInitialized",!1);R(this,"_onteardown");R(this,"_oncalltool");R(this,"_onlisttools");R(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=t,t.allowUnsafeEval||ee({jitless:!0}),this.setRequestHandler(Xn,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(!Kt.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=yh(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 sa(_,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 sa(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 aa(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await aa(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(Ch,(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(Zl,(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(Rl,(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},er,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},El,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},Ul,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let t=r.tools?Ml:Ll;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},Oh,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},go,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},jh,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},Ah,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 wh(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:kh}},Hh,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}}};R(Kt,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let fr=Kt;function ca(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 Wh{constructor(){R(this,"onhostcontextchanged");R(this,"onerror");R(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=ca(o),a=ca(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 Kh(){return globalThis.__S4L_BRIDGE__==="http"?new Wh:new fr({name:"S4L Panel",version:"1.0.0"})}function Hl(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 U=e=>document.getElementById(e),Fl=U("ver"),Xe=U("btn-setup"),Bt=U("btn-schedule"),Dt=U("stats-grid"),rr=U("stats-toggle"),Gh=U("log"),Qh=U("install-card"),Ne=U("setup-summary"),Jl=U("onboarding-details"),Yh=U("onboarding-steps"),ir=U("onboarding-blocker"),ua=U("onboarding-count"),Xh=U("onboarding-bar-fill"),eg=U("live-card"),tg=U("scanner-card"),Et=U("scanner-status"),hr=U("btn-scan-now"),ng=U("stats-card"),la=U("install-steps"),Ae=U("install-err"),be=U("btn-install"),rg=U("menubar-banner"),da=U("btn-menubar-restart"),Eo=U("btn-live"),Do=U("btn-live-stop"),ma=U("btn-live-front"),Le=U("live-status"),Vt=U("live-img"),Bl=U("switch-personal"),Vl=U("switch-promo"),_e=U("switch-pause"),ig=U("pause-name"),og=U("split-row"),ag=U("split-desc"),$e=U("split-slider"),Re=U("posting-volume-select"),sg=U("posting-volume-desc"),cg=U("mode-sub"),ug=U("settings-card"),or=U("settings-toggle"),fe=U("settings-body");let j=null,ar=!1,Ct=!1,sr=!1,ke=!1,tr=!1,Qe=!1,De=!1,cr=!1;function A(e){Gh.textContent=e}function lg(e){switch(e){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function Co(e){if(!e||!Array.isArray(e.steps)){la.innerHTML="";return}la.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">${lg(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),e.error?(Ae.textContent=e.error,Ae.hidden=!1):Ae.hidden=!0}const dg={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",reddit_connected:"Reddit connected",reddit_verified:"Reddit verified"};function mg(e){switch(e){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function gr(){Jl.hidden=!ke,Ne.setAttribute("aria-expanded",String(ke)),Ne.classList.toggle("expanded",ke)}function Nt(e){return e?e.setup_complete!==void 0?!!e.setup_complete:!!e.runtime_ready&&(e.projects_ready||0)>0&&!!e.x_connected:!1}function pg(e){if(!e||!Array.isArray(e.milestones)){Ne.hidden=!0,Jl.hidden=!0;return}const n=Nt(j);Ne.hidden=n,n&&ke&&(ke=!1,gr());const r=e.milestones.length,o=e.milestones.filter(i=>i.status==="complete").length,t=!!e.current_blocker&&!n;Ne.classList.toggle("complete",n),Ne.classList.toggle("blocked",t),ua.hidden=n,ua.textContent=t?`${o}/${r} · needs you`:Ct?`${o}/${r} · setting up…`:`${o}/${r}`,Xh.style.width=r>0?`${Math.round(o/r*100)}%`:"0%",Yh.innerHTML=e.milestones.map(i=>{const a=dg[i.id]||i.id,s=i.attempts>1?` <span class="detail">${i.attempts} attempts</span>`:"";return`<li class="${i.status}"><span class="glyph">${mg(i.status)}</span><span>${a}${s}</span></li>`}).join(""),e.current_blocker?(ir.textContent=`Current blocker: ${e.current_blocker.message}`,ir.hidden=!1,ke=!0):ir.hidden=!0,gr()}function Wt(){if(!j)return;pg(j.onboarding),Fl.innerHTML=j.update_available&&j.latest_version?`v${j.version} · <button id="btn-update" class="update-btn">Update to ${j.latest_version}</button>`:`v${j.version}`;const e=!j.runtime_ready;Qh.hidden=!e,rg.hidden=e||j.menubar_running!==!1;const n=Nt(j);Xe.hidden=n,Xe.disabled=!1,Xe.classList.toggle("primary",!n);const r=n&&(j.schedule_state==="missing"||j.schedule_state==="disabled"||j.schedule_state==="stalled");Bt.hidden=!r,Bt.classList.toggle("primary",r);const o=j.flags||(j.mode==="promotion"?{personal_brand:!1,promotion:!0}:{personal_brand:!0,promotion:!1}),t=!!o.personal_brand,i=!!o.promotion;Bl.setAttribute("aria-checked",String(t)),Vl.setAttribute("aria-checked",String(i));const a=Math.round((typeof j.personal_brand_share=="number"?j.personal_brand_share:.5)*100);og.hidden=!(t&&i),tr||($e.value=String(a),Yl(a)),cg.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.":"";const s=j.paused===!0;_e.setAttribute("aria-checked",String(!s)),_e.setAttribute("aria-label",s?"Resume S4L":"Pause S4L"),_e.title=s?"Resume S4L":"Pause S4L",ig.textContent=s?"Paused":"Running",eg.hidden=!n,ng.hidden=!n,ug.hidden=e,Wl()}let vr=null;const fg={scanning:"Scanning",drafting:"Drafting",posting:"Posting"};function hg(e){const n=Math.max(0,Math.round(e));return n<60?`${n}s`:`${Math.floor(n/60)}:${String(n%60).padStart(2,"0")}`}function Wl(){if(!j)return;const e=Nt(j);if(tg.hidden=!e,!e)return;const n=j.activity_state||"idle",r=j.paused===!0,o=n==="scanning",t=fg[n];if(hr.disabled=r||o,t)Et.textContent=`⟳ ${t} now…`;else if(r)Et.textContent="Paused — resume S4L to run the scanner.";else if(vr!=null){const i=(vr-Date.now())/1e3;Et.textContent=i>0?`Next scan in ${hg(i)}`:"Next scan any moment…"}else Et.textContent="Next scan within a minute"}async function nr(){if(!(!j||!Nt(j)))try{const e=await K("scan_status");if(!e||typeof e!="object")return;vr=typeof e.next_run_secs=="number"?Date.now()+e.next_run_secs*1e3:null,ge({activity_state:e.activity_state,activity_label:e.activity_label,next_run_secs:typeof e.next_run_secs=="number"?e.next_run_secs:null,...typeof e.paused=="boolean"?{paused:e.paused}:{}})}catch{}}setInterval(()=>{j&&Nt(j)&&Wl()},1e3);setInterval(()=>{document.hidden||nr()},5e3);function ge(e){j={...j||{},...e},Wt()}function gg(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||(j==null?void 0:j.version)||"",latest_version:e.latest_version??null,update_available:!!e.update_available,mode:e.mode??(j==null?void 0:j.mode),flags:e.flags??(j==null?void 0:j.flags),personal_brand_share:e.personal_brand_share??(j==null?void 0:j.personal_brand_share),onboarding:e.onboarding}}const Ie=Kh();function Kl(e){var n,r,o;e.theme&&Jh(e.theme),(n=e.styles)!=null&&n.variables&&Bh(e.styles.variables),(o=(r=e.styles)==null?void 0:r.css)!=null&&o.fonts&&Vh(e.styles.css.fonts)}Ie.onhostcontextchanged=Kl;Ie.onerror=e=>console.error(e);Ie.ontoolresult=e=>{const n=Hl(e);n&&typeof n.projects_total=="number"&&(ge(n),n.runtime_ready?(Gl(),nr()):n.runtime_provisioning&&Ro())};async function K(e,n={}){const r=await Ie.callServerTool({name:e,arguments:n});return Hl(r)}async function ve(){A("Refreshing…");try{const[e,n]=await Promise.all([K("project_config",{status:!0}),K("runtime",{action:"status"}).catch(()=>({}))]);ge({...gg(e),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},...typeof n.menubar_running=="boolean"?{menubar_running:n.menubar_running}:{},...typeof n.paused=="boolean"?{paused:n.paused}:{},onboarding:n.onboarding||e.onboarding||(j==null?void 0:j.onboarding)}),j&&!j.runtime_ready&&n.provisioning&&Ro(),A(""),Gl(),nr()}catch(e){A("Refresh failed: "+((e==null?void 0:e.message)||e))}}async function Ro(){if(!ar){ar=!0,be.disabled=!0,be.textContent="Installing…";try{for(;;){const e=await K("runtime",{action:"status"}).catch(()=>({}));if(Co(e.progress??null),e.onboarding&&ge({onboarding:e.onboarding}),e.runtime_ready){ge({runtime_ready:!0}),A("Runtime installed; you're ready to set up."),ve();return}const n=e.progress??null;if(n&&n.done&&!n.ok){be.disabled=!1,be.textContent="Retry install",A("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{ar=!1}}}async function vg(){var r;if(Ct)return;Ct=!0,Wt();const e=Date.now(),n=1200*1e3;try{for(;;){const o=await K("runtime",{action:"status"}).catch(()=>({}));o.progress&&Co(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&&ge(t),(r=o.onboarding)!=null&&r.complete){await ve(),A("Setup complete.");break}if(Date.now()-e>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{Ct=!1,Wt()}}async function Gl(){try{const e=await K("get_stats",{days:7}),n=Array.isArray(e.projects)?e.projects[0]:null,r=n==null?void 0:n.posts;if(!r){Dt.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]];Dt.innerHTML=o.map(([t,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${t}</div></div>`).join("")}catch(e){Dt.innerHTML=`<div class="muted">Stats unavailable: ${(e==null?void 0:e.message)||e}</div>`}}function Ot(e,n,r){const o=e.textContent;e.disabled=!0,e.textContent=n,r().finally(()=>{e.disabled=!1,e.textContent=o,Wt()})}Xe.addEventListener("click",()=>Ot(Xe,"Starting…",async()=>{A("Asking Claude to run setup…");try{const e=await Ie.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?A("The host rejected the setup request — type “set up S4L” in the chat instead."):(A("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),vg())}catch(e){A("Couldn’t start setup: "+((e==null?void 0:e.message)||e))}}));hr.addEventListener("click",()=>Ot(hr,"Starting…",async()=>{try{const e=await K("scan_now");e!=null&&e.paused?A("S4L is paused — resume it first, then the scanner can run."):e!=null&&e.ok?A("Scanner started — new drafts land in a few minutes."):A("Couldn’t start the scanner: "+((e==null?void 0:e.detail)||"unknown error"))}catch(e){A("Couldn’t start the scanner: "+((e==null?void 0:e.message)||e))}await nr()}));Bt.addEventListener("click",()=>Ot(Bt,"Setting up…",async()=>{A("Asking Claude to schedule the draft tasks for this account…"),K("client_event",{event:"rearm_clicked",surface:"panel"}).catch(()=>{});try{const e=await Ie.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?A("The host rejected it — type “set up the draft schedule” in the chat instead."):A("Scheduling is running in the chat. The draft tasks will register for this account.")}catch(e){A("Couldn’t start scheduling: "+((e==null?void 0:e.message)||e))}}));function Ql(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 K("engagement_mode",{action:"toggle",lane:n});r&&r.flags&&ge({flags:r.flags}),await ve()}catch(r){A("Couldn’t switch lane: "+((r==null?void 0:r.message)||r)),await ve()}finally{e.disabled=!1}}})}Ql(Bl,"personal_brand");Ql(Vl,"promotion");_e.addEventListener("click",async()=>{if(_e.disabled)return;const e=_e.getAttribute("aria-checked")!=="true",n=e?"resume":"pause";_e.disabled=!0,_e.setAttribute("aria-checked",String(e));try{const r=await K("pause_s4l",{action:n});A(r!=null&&r.ok?n==="pause"?"S4L paused — drafting and posting are stopped.":"S4L resumed.":`Couldn’t ${n} S4L${r!=null&&r.detail?": "+r.detail:"."}`),await ve()}catch(r){A(`Couldn’t ${n} S4L: `+((r==null?void 0:r.message)||r)),await ve()}finally{_e.disabled=!1}});function Yl(e){ag.textContent=`${e}% personal brand / ${100-e}% promotion`}$e.addEventListener("pointerdown",()=>{tr=!0});$e.addEventListener("input",()=>{tr=!0,Yl(Number($e.value))});$e.addEventListener("change",async()=>{const e=Number($e.value);$e.disabled=!0;try{const n=await K("engagement_mode",{action:"split",split:e});n&&typeof n.personal_brand_share=="number"&&ge({personal_brand_share:n.personal_brand_share}),await ve()}catch(n){A("Couldn’t set the lane split: "+((n==null?void 0:n.message)||n)),await ve()}finally{$e.disabled=!1,tr=!1}});let Xl=!1;function _g(e){if(!e||e.error)return;const n=e.mode||"medium";Re.value=n;const r=Array.isArray(e.rates)?e.rates:[],o={high:"Aggressive",medium:"Steady",low:"Chill"};for(const t of r){const i=Re.querySelector(`option[value="${t.mode}"]`);if(i&&t.est_posts_per_day!==null&&t.est_posts_per_day!==void 0){const a=Number(t.est_posts_per_day),s=o[t.mode]||t.mode;i.textContent=`${s} (~${a>=10?Math.round(a):a}/day)`}}sg.textContent="How many drafts per day the cycle produces."}async function _r(){try{const e=await K("posting_volume",{action:"get"});Xl=!0,_g(e)}catch{}}Re.addEventListener("change",async()=>{const e=Re.value;Re.disabled=!0;try{const n=await K("posting_volume",{action:"set",mode:e});n&&n.error?A("Couldn’t change posting volume: "+n.error):A(`Posting volume set to ${{high:"Aggressive",medium:"Steady",low:"Chill"}[e]||e}. Applies from the next cycle.`),await _r()}catch(n){A("Couldn’t change posting volume: "+((n==null?void 0:n.message)||n)),await _r()}finally{Re.disabled=!1}});Ne.addEventListener("click",()=>{ke=!ke,gr()});rr.addEventListener("click",()=>{Qe=!Qe,Dt.hidden=!Qe,rr.setAttribute("aria-expanded",String(Qe)),rr.classList.toggle("expanded",Qe)});Fl.addEventListener("click",e=>{const n=e.target;n&&n.id==="btn-update"&&bg()});async function bg(){if(sr)return;sr=!0;const e=document.getElementById("btn-update");e&&(e.disabled=!0,e.textContent="Updating…"),A("Installing the latest release… this can take a minute.");try{const n=await K("runtime",{action:"update"});n.ok?(A(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),e&&(e.textContent="Update installed — restart to apply")):(A("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){A("Update failed: "+((n==null?void 0:n.message)||n)),e&&(e.disabled=!1,e.textContent="Retry update")}finally{sr=!1}}be.addEventListener("click",async()=>{Ae.hidden=!0,be.disabled=!0,be.textContent="Starting…",A("Installing the runtime — this is a one-time download (~150MB+).");try{const e=await K("runtime",{action:"install"});if(e.runtime_ready){ge({runtime_ready:!0}),ve();return}Co(e.progress??null),Ro()}catch(e){be.disabled=!1,be.textContent="Retry install",Ae.textContent="Couldn't start install: "+((e==null?void 0:e.message)||e),Ae.hidden=!1}});da.addEventListener("click",()=>Ot(da,"Restarting…",async()=>{A("Restarting the S4L menu bar…");try{const e=await K("restart_menubar");typeof e.menubar_running=="boolean"&&ge({menubar_running:e.menubar_running}),A(e.menubar_running?"Menu bar restarted.":"Couldn’t confirm the menu bar came back"+(e.detail?": "+e.detail:"."))}catch(e){A("Couldn’t restart the menu bar: "+((e==null?void 0:e.message)||e))}}));const yg=["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"],$g=["description","voice","search_topics","content_angle","content_guardrails"],br={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"},pa=new Set(["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"]),kg=new Set(["website","get_started_link"]);function et(e){const n=e.replace(/[_-]+/g," ").trim();return n?n.charAt(0).toUpperCase()+n.slice(1):e}function ed(e){return!!e&&typeof e=="object"&&!Array.isArray(e)}function fa(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:D([u("web"),u("desktop"),u("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:m({touch:F().optional().describe("Whether the device supports touch input."),hover:F().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(),qh=m({method:u("ui/notifications/host-context-changed"),params:ql.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:x(jt).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:L(d(),J().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:Yn.describe("App identification (name and version)."),appCapabilities:Zh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var Hh=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Yn.describe("Host application identification and version."),hostCapabilities:Rh.describe("Features and capabilities provided by the host."),hostContext:ql.describe("Rich context about the host environment.")}).passthrough(),Fh={target:"draft-2020-12"};async function aa(e,n){let r=e["~standard"];if(r.jsonSchema)return r.jsonSchema[n](Fh);if(r.vendor==="zod"){let{z:o}=await ad(()=>Promise.resolve().then(()=>Tp),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 sa(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 Jh(e){let n=document.documentElement;n.setAttribute("data-theme",e),n.style.colorScheme=e}function Bh(e,n=document.documentElement){for(let[r,o]of Object.entries(e))o!==void 0&&n.style.setProperty(r,o)}function Vh(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 Kt=class Kt extends $h{constructor(r,o={},t={autoResize:!0}){super(t);R(this,"_appInfo");R(this,"_capabilities");R(this,"options");R(this,"_hostCapabilities");R(this,"_hostInfo");R(this,"_hostContext");R(this,"_registeredTools",{});R(this,"_initializedSent",!1);R(this,"eventSchemas",{toolinput:Th,toolinputpartial:Ph,toolresult:Mh,toolcancelled:Uh,hostcontextchanged:qh});R(this,"_everHadListener",new Set);R(this,"_toolHandlersInitialized",!1);R(this,"_onteardown");R(this,"_oncalltool");R(this,"_onlisttools");R(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=t,t.allowUnsafeEval||ee({jitless:!0}),this.setRequestHandler(Xn,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(!Kt.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=yh(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 sa(_,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 sa(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 aa(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await aa(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(Ch,(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(Zl,(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(Rl,(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},er,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},El,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},Ul,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let t=r.tools?Ml:Ll;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},Oh,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},go,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},jh,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},Ah,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 wh(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:kh}},Hh,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}}};R(Kt,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let fr=Kt;function ca(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 Wh{constructor(){R(this,"onhostcontextchanged");R(this,"onerror");R(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=ca(o),a=ca(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 Kh(){return globalThis.__S4L_BRIDGE__==="http"?new Wh:new fr({name:"S4L Panel",version:"1.0.0"})}function Hl(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 U=e=>document.getElementById(e),Fl=U("ver"),Xe=U("btn-setup"),Bt=U("btn-schedule"),Dt=U("stats-grid"),rr=U("stats-toggle"),Gh=U("log"),Qh=U("install-card"),Ne=U("setup-summary"),Jl=U("onboarding-details"),Yh=U("onboarding-steps"),ir=U("onboarding-blocker"),ua=U("onboarding-count"),Xh=U("onboarding-bar-fill"),eg=U("live-card"),tg=U("drafting-card"),Et=U("drafting-status"),hr=U("btn-run-drafting"),ng=U("stats-card"),la=U("install-steps"),Ae=U("install-err"),be=U("btn-install"),rg=U("menubar-banner"),da=U("btn-menubar-restart"),Eo=U("btn-live"),Do=U("btn-live-stop"),ma=U("btn-live-front"),Le=U("live-status"),Vt=U("live-img"),Bl=U("switch-personal"),Vl=U("switch-promo"),_e=U("switch-pause"),ig=U("pause-name"),og=U("split-row"),ag=U("split-desc"),$e=U("split-slider"),Re=U("posting-volume-select"),sg=U("posting-volume-desc"),cg=U("mode-sub"),ug=U("settings-card"),or=U("settings-toggle"),fe=U("settings-body");let j=null,ar=!1,Ct=!1,sr=!1,ke=!1,tr=!1,Qe=!1,De=!1,cr=!1;function A(e){Gh.textContent=e}function lg(e){switch(e){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function Co(e){if(!e||!Array.isArray(e.steps)){la.innerHTML="";return}la.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">${lg(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),e.error?(Ae.textContent=e.error,Ae.hidden=!1):Ae.hidden=!0}const dg={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",reddit_connected:"Reddit connected",reddit_verified:"Reddit verified"};function mg(e){switch(e){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function gr(){Jl.hidden=!ke,Ne.setAttribute("aria-expanded",String(ke)),Ne.classList.toggle("expanded",ke)}function Nt(e){return e?e.setup_complete!==void 0?!!e.setup_complete:!!e.runtime_ready&&(e.projects_ready||0)>0&&!!e.x_connected:!1}function pg(e){if(!e||!Array.isArray(e.milestones)){Ne.hidden=!0,Jl.hidden=!0;return}const n=Nt(j);Ne.hidden=n,n&&ke&&(ke=!1,gr());const r=e.milestones.length,o=e.milestones.filter(i=>i.status==="complete").length,t=!!e.current_blocker&&!n;Ne.classList.toggle("complete",n),Ne.classList.toggle("blocked",t),ua.hidden=n,ua.textContent=t?`${o}/${r} · needs you`:Ct?`${o}/${r} · setting up…`:`${o}/${r}`,Xh.style.width=r>0?`${Math.round(o/r*100)}%`:"0%",Yh.innerHTML=e.milestones.map(i=>{const a=dg[i.id]||i.id,s=i.attempts>1?` <span class="detail">${i.attempts} attempts</span>`:"";return`<li class="${i.status}"><span class="glyph">${mg(i.status)}</span><span>${a}${s}</span></li>`}).join(""),e.current_blocker?(ir.textContent=`Current blocker: ${e.current_blocker.message}`,ir.hidden=!1,ke=!0):ir.hidden=!0,gr()}function Wt(){if(!j)return;pg(j.onboarding),Fl.innerHTML=j.update_available&&j.latest_version?`v${j.version} · <button id="btn-update" class="update-btn">Update to ${j.latest_version}</button>`:`v${j.version}`;const e=!j.runtime_ready;Qh.hidden=!e,rg.hidden=e||j.menubar_running!==!1;const n=Nt(j);Xe.hidden=n,Xe.disabled=!1,Xe.classList.toggle("primary",!n);const r=n&&(j.schedule_state==="missing"||j.schedule_state==="disabled"||j.schedule_state==="stalled");Bt.hidden=!r,Bt.classList.toggle("primary",r);const o=j.flags||(j.mode==="promotion"?{personal_brand:!1,promotion:!0}:{personal_brand:!0,promotion:!1}),t=!!o.personal_brand,i=!!o.promotion;Bl.setAttribute("aria-checked",String(t)),Vl.setAttribute("aria-checked",String(i));const a=Math.round((typeof j.personal_brand_share=="number"?j.personal_brand_share:.5)*100);og.hidden=!(t&&i),tr||($e.value=String(a),Yl(a)),cg.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.":"";const s=j.paused===!0;_e.setAttribute("aria-checked",String(!s)),_e.setAttribute("aria-label",s?"Resume S4L":"Pause S4L"),_e.title=s?"Resume S4L":"Pause S4L",ig.textContent=s?"Paused":"Running",eg.hidden=!n,ng.hidden=!n,ug.hidden=e,Wl()}let vr=null;const fg={scanning:"Scanning",drafting:"Drafting",posting:"Posting"};function hg(e){const n=Math.max(0,Math.round(e));return n<60?`${n}s`:`${Math.floor(n/60)}:${String(n%60).padStart(2,"0")}`}function Wl(){if(!j)return;const e=Nt(j);if(tg.hidden=!e,!e)return;const n=j.activity_state||"idle",r=j.paused===!0,o=n==="scanning"||n==="drafting"||n==="posting",t=fg[n];if(hr.disabled=r||o,t)Et.textContent=`⟳ ${t} now…`;else if(r)Et.textContent="Paused — resume S4L to run drafting.";else if(vr!=null){const i=(vr-Date.now())/1e3;Et.textContent=i>0?`Next drafting run in ${hg(i)}`:"Next drafting run any moment…"}else Et.textContent="Next drafting run within a minute"}async function nr(){if(!(!j||!Nt(j)))try{const e=await K("drafting_status");if(!e||typeof e!="object")return;vr=typeof e.next_run_secs=="number"?Date.now()+e.next_run_secs*1e3:null,ge({activity_state:e.activity_state,activity_label:e.activity_label,next_run_secs:typeof e.next_run_secs=="number"?e.next_run_secs:null,...typeof e.paused=="boolean"?{paused:e.paused}:{}})}catch{}}setInterval(()=>{j&&Nt(j)&&Wl()},1e3);setInterval(()=>{document.hidden||nr()},5e3);function ge(e){j={...j||{},...e},Wt()}function gg(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||(j==null?void 0:j.version)||"",latest_version:e.latest_version??null,update_available:!!e.update_available,mode:e.mode??(j==null?void 0:j.mode),flags:e.flags??(j==null?void 0:j.flags),personal_brand_share:e.personal_brand_share??(j==null?void 0:j.personal_brand_share),onboarding:e.onboarding}}const Ie=Kh();function Kl(e){var n,r,o;e.theme&&Jh(e.theme),(n=e.styles)!=null&&n.variables&&Bh(e.styles.variables),(o=(r=e.styles)==null?void 0:r.css)!=null&&o.fonts&&Vh(e.styles.css.fonts)}Ie.onhostcontextchanged=Kl;Ie.onerror=e=>console.error(e);Ie.ontoolresult=e=>{const n=Hl(e);n&&typeof n.projects_total=="number"&&(ge(n),n.runtime_ready?(Gl(),nr()):n.runtime_provisioning&&Ro())};async function K(e,n={}){const r=await Ie.callServerTool({name:e,arguments:n});return Hl(r)}async function ve(){A("Refreshing…");try{const[e,n]=await Promise.all([K("project_config",{status:!0}),K("runtime",{action:"status"}).catch(()=>({}))]);ge({...gg(e),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},...typeof n.menubar_running=="boolean"?{menubar_running:n.menubar_running}:{},...typeof n.paused=="boolean"?{paused:n.paused}:{},onboarding:n.onboarding||e.onboarding||(j==null?void 0:j.onboarding)}),j&&!j.runtime_ready&&n.provisioning&&Ro(),A(""),Gl(),nr()}catch(e){A("Refresh failed: "+((e==null?void 0:e.message)||e))}}async function Ro(){if(!ar){ar=!0,be.disabled=!0,be.textContent="Installing…";try{for(;;){const e=await K("runtime",{action:"status"}).catch(()=>({}));if(Co(e.progress??null),e.onboarding&&ge({onboarding:e.onboarding}),e.runtime_ready){ge({runtime_ready:!0}),A("Runtime installed; you're ready to set up."),ve();return}const n=e.progress??null;if(n&&n.done&&!n.ok){be.disabled=!1,be.textContent="Retry install",A("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{ar=!1}}}async function vg(){var r;if(Ct)return;Ct=!0,Wt();const e=Date.now(),n=1200*1e3;try{for(;;){const o=await K("runtime",{action:"status"}).catch(()=>({}));o.progress&&Co(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&&ge(t),(r=o.onboarding)!=null&&r.complete){await ve(),A("Setup complete.");break}if(Date.now()-e>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{Ct=!1,Wt()}}async function Gl(){try{const e=await K("get_stats",{days:7}),n=Array.isArray(e.projects)?e.projects[0]:null,r=n==null?void 0:n.posts;if(!r){Dt.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]];Dt.innerHTML=o.map(([t,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${t}</div></div>`).join("")}catch(e){Dt.innerHTML=`<div class="muted">Stats unavailable: ${(e==null?void 0:e.message)||e}</div>`}}function Ot(e,n,r){const o=e.textContent;e.disabled=!0,e.textContent=n,r().finally(()=>{e.disabled=!1,e.textContent=o,Wt()})}Xe.addEventListener("click",()=>Ot(Xe,"Starting…",async()=>{A("Asking Claude to run setup…");try{const e=await Ie.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?A("The host rejected the setup request — type “set up S4L” in the chat instead."):(A("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),vg())}catch(e){A("Couldn’t start setup: "+((e==null?void 0:e.message)||e))}}));hr.addEventListener("click",()=>Ot(hr,"Starting…",async()=>{try{const e=await K("run_drafting");e!=null&&e.paused?A("S4L is paused — resume it first, then drafting can run."):e!=null&&e.ok?A("Drafting started — new drafts land in a few minutes."):A("Couldn’t start drafting: "+((e==null?void 0:e.detail)||"unknown error"))}catch(e){A("Couldn’t start drafting: "+((e==null?void 0:e.message)||e))}await nr()}));Bt.addEventListener("click",()=>Ot(Bt,"Setting up…",async()=>{A("Asking Claude to schedule the draft tasks for this account…"),K("client_event",{event:"rearm_clicked",surface:"panel"}).catch(()=>{});try{const e=await Ie.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?A("The host rejected it — type “set up the draft schedule” in the chat instead."):A("Scheduling is running in the chat. The draft tasks will register for this account.")}catch(e){A("Couldn’t start scheduling: "+((e==null?void 0:e.message)||e))}}));function Ql(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 K("engagement_mode",{action:"toggle",lane:n});r&&r.flags&&ge({flags:r.flags}),await ve()}catch(r){A("Couldn’t switch lane: "+((r==null?void 0:r.message)||r)),await ve()}finally{e.disabled=!1}}})}Ql(Bl,"personal_brand");Ql(Vl,"promotion");_e.addEventListener("click",async()=>{if(_e.disabled)return;const e=_e.getAttribute("aria-checked")!=="true",n=e?"resume":"pause";_e.disabled=!0,_e.setAttribute("aria-checked",String(e));try{const r=await K("pause_s4l",{action:n});A(r!=null&&r.ok?n==="pause"?"S4L paused — drafting and posting are stopped.":"S4L resumed.":`Couldn’t ${n} S4L${r!=null&&r.detail?": "+r.detail:"."}`),await ve()}catch(r){A(`Couldn’t ${n} S4L: `+((r==null?void 0:r.message)||r)),await ve()}finally{_e.disabled=!1}});function Yl(e){ag.textContent=`${e}% personal brand / ${100-e}% promotion`}$e.addEventListener("pointerdown",()=>{tr=!0});$e.addEventListener("input",()=>{tr=!0,Yl(Number($e.value))});$e.addEventListener("change",async()=>{const e=Number($e.value);$e.disabled=!0;try{const n=await K("engagement_mode",{action:"split",split:e});n&&typeof n.personal_brand_share=="number"&&ge({personal_brand_share:n.personal_brand_share}),await ve()}catch(n){A("Couldn’t set the lane split: "+((n==null?void 0:n.message)||n)),await ve()}finally{$e.disabled=!1,tr=!1}});let Xl=!1;function _g(e){if(!e||e.error)return;const n=e.mode||"medium";Re.value=n;const r=Array.isArray(e.rates)?e.rates:[],o={high:"Aggressive",medium:"Steady",low:"Chill"};for(const t of r){const i=Re.querySelector(`option[value="${t.mode}"]`);if(i&&t.est_posts_per_day!==null&&t.est_posts_per_day!==void 0){const a=Number(t.est_posts_per_day),s=o[t.mode]||t.mode;i.textContent=`${s} (~${a>=10?Math.round(a):a}/day)`}}sg.textContent="How many drafts per day the cycle produces."}async function _r(){try{const e=await K("posting_volume",{action:"get"});Xl=!0,_g(e)}catch{}}Re.addEventListener("change",async()=>{const e=Re.value;Re.disabled=!0;try{const n=await K("posting_volume",{action:"set",mode:e});n&&n.error?A("Couldn’t change posting volume: "+n.error):A(`Posting volume set to ${{high:"Aggressive",medium:"Steady",low:"Chill"}[e]||e}. Applies from the next cycle.`),await _r()}catch(n){A("Couldn’t change posting volume: "+((n==null?void 0:n.message)||n)),await _r()}finally{Re.disabled=!1}});Ne.addEventListener("click",()=>{ke=!ke,gr()});rr.addEventListener("click",()=>{Qe=!Qe,Dt.hidden=!Qe,rr.setAttribute("aria-expanded",String(Qe)),rr.classList.toggle("expanded",Qe)});Fl.addEventListener("click",e=>{const n=e.target;n&&n.id==="btn-update"&&bg()});async function bg(){if(sr)return;sr=!0;const e=document.getElementById("btn-update");e&&(e.disabled=!0,e.textContent="Updating…"),A("Installing the latest release… this can take a minute.");try{const n=await K("runtime",{action:"update"});n.ok?(A(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),e&&(e.textContent="Update installed — restart to apply")):(A("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){A("Update failed: "+((n==null?void 0:n.message)||n)),e&&(e.disabled=!1,e.textContent="Retry update")}finally{sr=!1}}be.addEventListener("click",async()=>{Ae.hidden=!0,be.disabled=!0,be.textContent="Starting…",A("Installing the runtime — this is a one-time download (~150MB+).");try{const e=await K("runtime",{action:"install"});if(e.runtime_ready){ge({runtime_ready:!0}),ve();return}Co(e.progress??null),Ro()}catch(e){be.disabled=!1,be.textContent="Retry install",Ae.textContent="Couldn't start install: "+((e==null?void 0:e.message)||e),Ae.hidden=!1}});da.addEventListener("click",()=>Ot(da,"Restarting…",async()=>{A("Restarting the S4L menu bar…");try{const e=await K("restart_menubar");typeof e.menubar_running=="boolean"&&ge({menubar_running:e.menubar_running}),A(e.menubar_running?"Menu bar restarted.":"Couldn’t confirm the menu bar came back"+(e.detail?": "+e.detail:"."))}catch(e){A("Couldn’t restart the menu bar: "+((e==null?void 0:e.message)||e))}}));const yg=["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"],$g=["description","voice","search_topics","content_angle","content_guardrails"],br={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"},pa=new Set(["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"]),kg=new Set(["website","get_started_link"]);function et(e){const n=e.replace(/[_-]+/g," ").trim();return n?n.charAt(0).toUpperCase()+n.slice(1):e}function ed(e){return!!e&&typeof e=="object"&&!Array.isArray(e)}function fa(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 ha(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 Sg(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?$g:yg;for(const f of a){const _=e.fields[f];if(ed(_)&&Object.keys(_).length){const z=document.createElement("fieldset");z.className="settings-group";const S=document.createElement("legend");S.textContent=br[f]||et(f),z.appendChild(S);for(const[g,$]of Object.entries(_)){const y=fa($),I=ha(et(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(_??"")}:fa(_),b=ha(br[f]||et(f),w,kg.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 wg(e,i,c,p)),n}async function wg(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=>(br[h.key]||et(h.key))+(h.sub?` · ${et(h.sub)}`:""),c=[...new Set(t.filter(h=>h.sub!==void 0).map(h=>h.key))];for(const h of c){const f=ed(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(`
|
|
@@ -198,18 +198,18 @@ container holding the app. Specify either width or maxWidth, and either height o
|
|
|
198
198
|
<div class="lane-note" id="mode-sub"></div>
|
|
199
199
|
</section>
|
|
200
200
|
|
|
201
|
-
<!--
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
post-setup
|
|
205
|
-
<section class="section" id="
|
|
201
|
+
<!-- Drafting pipeline: the cycle the user triggers — it scans for threads
|
|
202
|
+
first, then hands off to the drafter. A live status / countdown line +
|
|
203
|
+
a one-click Run-now, mirroring the menu-bar dropdown. Both read
|
|
204
|
+
scripts/live_status.py so the surfaces can't drift. Shown post-setup. -->
|
|
205
|
+
<section class="section" id="drafting-card" hidden>
|
|
206
206
|
<div class="section-row">
|
|
207
|
-
<span class="section-label">
|
|
207
|
+
<span class="section-label">Drafting</span>
|
|
208
208
|
<div class="section-actions">
|
|
209
|
-
<button id="btn-
|
|
209
|
+
<button id="btn-run-drafting" class="ghost">Run drafting now</button>
|
|
210
210
|
</div>
|
|
211
211
|
</div>
|
|
212
|
-
<div class="live-status" id="
|
|
212
|
+
<div class="live-status" id="drafting-status"></div>
|
|
213
213
|
</section>
|
|
214
214
|
|
|
215
215
|
<section class="section" id="live-card" hidden>
|
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.6-rc.
|
|
5
|
+
"version": "1.7.6-rc.32",
|
|
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": {
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"description": "Restart the S4L menu bar app"
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
|
-
"name": "
|
|
53
|
-
"description": "Run the
|
|
52
|
+
"name": "run_drafting",
|
|
53
|
+
"description": "Run the drafting pipeline now"
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
|
-
"name": "
|
|
57
|
-
"description": "Live
|
|
56
|
+
"name": "drafting_status",
|
|
57
|
+
"description": "Live drafting-pipeline status + next-run countdown"
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
"name": "posting_volume",
|
|
@@ -291,10 +291,10 @@ STOP_FLAG = os.path.join(st.state_dir(), "stopped.flag")
|
|
|
291
291
|
# pauseFlagPath() in mcp/src/index.ts.
|
|
292
292
|
PAUSE_FLAG = os.path.join(st.state_dir(), "paused.flag")
|
|
293
293
|
|
|
294
|
-
# The launchd kicker label for the
|
|
295
|
-
#
|
|
296
|
-
# the MCP server / Desktop is closed. Keep in sync with
|
|
297
|
-
# in mcp/src/index.ts.
|
|
294
|
+
# The launchd kicker label for the drafting pipeline (the twitter cycle: scan,
|
|
295
|
+
# then draft). "Run drafting now" kickstarts it directly — Claude-independent, so
|
|
296
|
+
# it works even when the MCP server / Desktop is closed. Keep in sync with
|
|
297
|
+
# TWITTER_AUTOPILOT_LABEL in mcp/src/index.ts.
|
|
298
298
|
TWITTER_CYCLE_LABEL = "com.m13v.social-twitter-cycle"
|
|
299
299
|
|
|
300
300
|
# Autopilot scheduled tasks. Queue workers must RUN in a dedicated folder
|
|
@@ -2911,12 +2911,12 @@ class S4LMenuBar(rumps.App):
|
|
|
2911
2911
|
self._channel = snap.get("channel") or "stable"
|
|
2912
2912
|
self._latest_tag = snap.get("latest_tag")
|
|
2913
2913
|
|
|
2914
|
-
# Live pipeline status + next-
|
|
2914
|
+
# Live drafting-pipeline status + next-run countdown — ONE shared module
|
|
2915
2915
|
# (scripts/live_status.py) the dashboard widget reads via snapshot.py too,
|
|
2916
2916
|
# so the tray and the widget can't drift. Cheap (two small file reads),
|
|
2917
2917
|
# computed fresh here rather than off the 4s-cached snap so the countdown
|
|
2918
2918
|
# stays honest. Best-effort: any failure leaves an empty dict and the menu
|
|
2919
|
-
# simply omits the
|
|
2919
|
+
# simply omits the drafting status/countdown lines.
|
|
2920
2920
|
try:
|
|
2921
2921
|
import live_status as _live_status
|
|
2922
2922
|
|
|
@@ -2975,7 +2975,7 @@ class S4LMenuBar(rumps.App):
|
|
|
2975
2975
|
# View mode: rebuilds when the review layout changes (checkmark
|
|
2976
2976
|
# in the "View Mode" submenu + whether the cadence item shows).
|
|
2977
2977
|
st.read_review_layout(),
|
|
2978
|
-
#
|
|
2978
|
+
# Drafting status + countdown (State C): rebuild when the live
|
|
2979
2979
|
# pipeline verb changes (idle <-> scanning/drafting/posting) and
|
|
2980
2980
|
# when the next-run countdown crosses a 15s bucket — bucketed so an
|
|
2981
2981
|
# open menu isn't torn down every second for a -1s tick.
|
|
@@ -4099,33 +4099,38 @@ class S4LMenuBar(rumps.App):
|
|
|
4099
4099
|
items += self._state_b(ob, blocker)
|
|
4100
4100
|
else:
|
|
4101
4101
|
items += self._state_c(snap, pending_count)
|
|
4102
|
-
#
|
|
4103
|
-
# pending drafts):
|
|
4104
|
-
#
|
|
4105
|
-
#
|
|
4106
|
-
#
|
|
4102
|
+
# Drafting-pipeline control (post-setup, every State-C menu regardless
|
|
4103
|
+
# of pending drafts): ONE line — the action is the phrase, the live
|
|
4104
|
+
# state is in brackets. The pipeline scans first, then hands off to the
|
|
4105
|
+
# drafter; this mirrors the dashboard widget, both reading
|
|
4106
|
+
# scripts/live_status.py so the surfaces can't drift.
|
|
4107
|
+
# idle -> "Run drafting now (next run in 42s)" [clickable]
|
|
4108
|
+
# running -> "Run drafting now (drafting…)" [greyed]
|
|
4109
|
+
# paused -> "Run drafting now (paused)" [greyed]
|
|
4110
|
+
# Clickable only when idle & not paused: while a run is in flight
|
|
4111
|
+
# launchd would just no-op the single instance, and while paused the
|
|
4112
|
+
# kicker is unloaded so nothing would run.
|
|
4107
4113
|
live = getattr(self, "_live", {}) or {}
|
|
4108
4114
|
act_state = live.get("activity_state", "idle")
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4115
|
+
running = act_state in ("scanning", "drafting", "posting")
|
|
4116
|
+
paused = os.path.exists(PAUSE_FLAG)
|
|
4117
|
+
if paused:
|
|
4118
|
+
_bracket = "paused"
|
|
4119
|
+
elif running:
|
|
4120
|
+
_bracket = f"{act_state}…"
|
|
4113
4121
|
else:
|
|
4114
4122
|
_secs = live.get("next_run_secs")
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
items.append(self._label("Run scan now (paused)"))
|
|
4125
|
-
elif act_state == "scanning":
|
|
4126
|
-
items.append(self._label("Run scan now (already scanning)"))
|
|
4123
|
+
_bracket = (
|
|
4124
|
+
f"next run in {self._fmt_countdown(_secs)}"
|
|
4125
|
+
if isinstance(_secs, int)
|
|
4126
|
+
else "next run within a minute"
|
|
4127
|
+
)
|
|
4128
|
+
_text = f"Run drafting now ({_bracket})"
|
|
4129
|
+
items.append(rumps.separator)
|
|
4130
|
+
if paused or running:
|
|
4131
|
+
items.append(self._label(_text)) # greyed: can't run right now
|
|
4127
4132
|
else:
|
|
4128
|
-
items.append(rumps.MenuItem(
|
|
4133
|
+
items.append(rumps.MenuItem(_text, callback=self._run_drafting))
|
|
4129
4134
|
|
|
4130
4135
|
# Engagement lanes — ALWAYS visible (every state), not just post-setup, so
|
|
4131
4136
|
# the user can see + flip either lane any time. Two INDEPENDENT checkmarks
|
|
@@ -4276,14 +4281,15 @@ class S4LMenuBar(rumps.App):
|
|
|
4276
4281
|
return f"{s}s"
|
|
4277
4282
|
return f"{s // 60}:{s % 60:02d}"
|
|
4278
4283
|
|
|
4279
|
-
def
|
|
4280
|
-
"""
|
|
4281
|
-
(com.m13v.social-twitter-cycle). Same mechanism the MCP's
|
|
4282
|
-
and the first-run onboarding kick use — the job runs
|
|
4283
|
-
its full baked env + singleton lock, so it is NOT a
|
|
4284
|
-
cycle. Done directly here (not via the loopback tool)
|
|
4285
|
-
when Claude Desktop / the MCP server is closed.
|
|
4286
|
-
thread so a slow launchctl never freezes the
|
|
4284
|
+
def _run_drafting(self, _=None):
|
|
4285
|
+
"""Run the drafting pipeline ONE time immediately by kickstarting the
|
|
4286
|
+
launchd kicker (com.m13v.social-twitter-cycle). Same mechanism the MCP's
|
|
4287
|
+
run_drafting tool and the first-run onboarding kick use — the job runs
|
|
4288
|
+
THROUGH launchd with its full baked env + singleton lock, so it is NOT a
|
|
4289
|
+
bare hand-run of the cycle. Done directly here (not via the loopback tool)
|
|
4290
|
+
so it works even when Claude Desktop / the MCP server is closed.
|
|
4291
|
+
Best-effort + off the UI thread so a slow launchctl never freezes the
|
|
4292
|
+
tray."""
|
|
4287
4293
|
def work():
|
|
4288
4294
|
try:
|
|
4289
4295
|
r = subprocess.run(
|
|
@@ -4292,11 +4298,11 @@ class S4LMenuBar(rumps.App):
|
|
|
4292
4298
|
timeout=15,
|
|
4293
4299
|
)
|
|
4294
4300
|
if r.returncode == 0:
|
|
4295
|
-
self._notify("S4L", "
|
|
4301
|
+
self._notify("S4L", "Drafting started — new drafts land in a few minutes.")
|
|
4296
4302
|
else:
|
|
4297
|
-
self._notify("S4L", "Couldn’t start
|
|
4303
|
+
self._notify("S4L", "Couldn’t start drafting. Try again in a moment.")
|
|
4298
4304
|
except Exception:
|
|
4299
|
-
self._notify("S4L", "Couldn’t start
|
|
4305
|
+
self._notify("S4L", "Couldn’t start drafting. Try again in a moment.")
|
|
4300
4306
|
# Repaint promptly so the status line flips to "Scanning now…" without
|
|
4301
4307
|
# waiting for the next natural rebuild.
|
|
4302
4308
|
self._sig = None
|
package/mcp/menubar/s4l_state.py
CHANGED
|
@@ -1282,11 +1282,16 @@ def write_review_layout(layout):
|
|
|
1282
1282
|
# Server-side per-install throttle for the twitter cycle's virality bar:
|
|
1283
1283
|
# high|medium|low|None (None = driver default). Source of truth is
|
|
1284
1284
|
# installations.posting_mode via /api/v1/installations/posting-mode; unlike
|
|
1285
|
-
# the lane flags there is NO local mode.json copy
|
|
1286
|
-
# remote adjustability (dashboard and menubar must
|
|
1285
|
+
# the lane flags there is NO local mode.json copy that RESOLVES the mode,
|
|
1286
|
+
# because the whole point is remote adjustability (dashboard and menubar must
|
|
1287
|
+
# agree with the server).
|
|
1287
1288
|
# Reads are cached so the menu rebuild tick never blocks on the network: a
|
|
1288
1289
|
# stale/missing cache kicks one background refresh and returns the last-known
|
|
1289
|
-
# value
|
|
1290
|
+
# value. The in-memory cache is seeded from a disk copy of the last server
|
|
1291
|
+
# answer (scripts/s4l_posting_mode.py cache file, 2026-07-17): without it,
|
|
1292
|
+
# every menubar restart (updates restart the menubar) displayed the default
|
|
1293
|
+
# "Steady" until the first refresh landed, which read as the user's chosen
|
|
1294
|
+
# mode having been reset. The seed never suppresses a refresh (at stays 0.0).
|
|
1290
1295
|
POSTING_MODE_TTL_SECS = 600
|
|
1291
1296
|
_posting_mode_cache = {"mode": None, "known": False, "at": 0.0}
|
|
1292
1297
|
_posting_mode_lock = threading.Lock()
|
|
@@ -1299,20 +1304,38 @@ def _scripts_on_path():
|
|
|
1299
1304
|
sys.path.insert(0, scripts)
|
|
1300
1305
|
|
|
1301
1306
|
|
|
1307
|
+
def _seed_posting_mode_from_disk():
|
|
1308
|
+
"""Seed the in-memory cache with the last server answer that any surface
|
|
1309
|
+
persisted (menubar, MCP tool, panel). at stays 0.0 so the first read still
|
|
1310
|
+
kicks a real refresh; the server answer overwrites the seed when it lands."""
|
|
1311
|
+
try:
|
|
1312
|
+
_scripts_on_path()
|
|
1313
|
+
from s4l_posting_mode import read_cached_mode
|
|
1314
|
+
|
|
1315
|
+
mode = read_cached_mode()
|
|
1316
|
+
if mode:
|
|
1317
|
+
with _posting_mode_lock:
|
|
1318
|
+
_posting_mode_cache["mode"] = mode
|
|
1319
|
+
except Exception:
|
|
1320
|
+
pass
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
_seed_posting_mode_from_disk()
|
|
1324
|
+
|
|
1325
|
+
|
|
1302
1326
|
def _refresh_posting_mode():
|
|
1303
1327
|
try:
|
|
1304
1328
|
_scripts_on_path()
|
|
1305
1329
|
from http_api import api_get
|
|
1330
|
+
from s4l_posting_mode import write_cached_mode
|
|
1306
1331
|
|
|
1307
1332
|
r = api_get("/api/v1/installations/posting-mode")
|
|
1308
1333
|
d = (r or {}).get("data") or {}
|
|
1309
1334
|
mode = d.get("mode")
|
|
1335
|
+
mode = mode if mode in ("high", "medium", "low") else None
|
|
1336
|
+
write_cached_mode(mode)
|
|
1310
1337
|
with _posting_mode_lock:
|
|
1311
|
-
_posting_mode_cache.update(
|
|
1312
|
-
mode=mode if mode in ("high", "medium", "low") else None,
|
|
1313
|
-
known=True,
|
|
1314
|
-
at=time.time(),
|
|
1315
|
-
)
|
|
1338
|
+
_posting_mode_cache.update(mode=mode, known=True, at=time.time())
|
|
1316
1339
|
except Exception as e:
|
|
1317
1340
|
sys.stderr.write(f"[s4l-state] posting-mode refresh failed: {e}\n")
|
|
1318
1341
|
with _posting_mode_lock:
|
package/mcp/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m13v/s4l-mcp",
|
|
3
|
-
"version": "1.7.6-rc.
|
|
3
|
+
"version": "1.7.6-rc.32",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Desktop MCP client for social-autoposter (X/Twitter rail): manual draft/review/approve loop, autopilot control, and stats. Thin wrapper over the existing pipeline scripts.",
|
|
6
6
|
"license": "MIT",
|
package/package.json
CHANGED
|
@@ -6,13 +6,21 @@ tool and the panel. The mode (high|medium|low|None) is stored SERVER-SIDE on
|
|
|
6
6
|
installations.posting_mode; the virality-threshold route maps it to a
|
|
7
7
|
percentile and overrides the cycle driver's request, so changes apply on the
|
|
8
8
|
install's next cycle with no client update. There is deliberately NO local
|
|
9
|
-
mode.json copy (dashboard, menubar, and panel must
|
|
10
|
-
server).
|
|
9
|
+
mode.json copy that RESOLVES the mode (dashboard, menubar, and panel must
|
|
10
|
+
always agree with the server).
|
|
11
|
+
|
|
12
|
+
Display cache (2026-07-17): a last-known copy of the server's answer IS kept
|
|
13
|
+
at <state_dir>/posting-mode.cache.json, written on every successful get/set
|
|
14
|
+
and read only when the network is unavailable (and by the menubar to seed its
|
|
15
|
+
boot display). Without it, every menubar/panel restart showed the default
|
|
16
|
+
"Steady" until the first network round trip landed, which read as the user's
|
|
17
|
+
chosen mode having been reset. The cache never overrides a server answer.
|
|
11
18
|
|
|
12
19
|
Usage:
|
|
13
20
|
python3 scripts/s4l_posting_mode.py get
|
|
14
21
|
-> {"mode": ..., "rates": [{mode,pctile,threshold,est_posts_per_day}...],
|
|
15
22
|
"pool_count": N, "batch_count": N, "window_days": 7}
|
|
23
|
+
(on network failure with a cache present: {"mode": ..., "cached": true})
|
|
16
24
|
python3 scripts/s4l_posting_mode.py set <high|medium|low|default>
|
|
17
25
|
-> {"mode": ...} ("default" clears the override)
|
|
18
26
|
|
|
@@ -21,6 +29,7 @@ Prints JSON on stdout; exits non-zero with an {"error": ...} JSON on failure.
|
|
|
21
29
|
import json
|
|
22
30
|
import os
|
|
23
31
|
import sys
|
|
32
|
+
import time
|
|
24
33
|
|
|
25
34
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
26
35
|
from http_api import api_get, api_post # noqa: E402
|
|
@@ -28,13 +37,60 @@ from http_api import api_get, api_post # noqa: E402
|
|
|
28
37
|
VALID = ("high", "medium", "low")
|
|
29
38
|
|
|
30
39
|
|
|
40
|
+
def _cache_path() -> str:
|
|
41
|
+
state_dir = os.environ.get("S4L_STATE_DIR") or os.path.join(
|
|
42
|
+
os.path.expanduser("~"), ".social-autoposter-mcp"
|
|
43
|
+
)
|
|
44
|
+
return os.path.join(state_dir, "posting-mode.cache.json")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_cached_mode():
|
|
48
|
+
"""Last-known SERVER mode ('high'|'medium'|'low') or None (unset/no cache).
|
|
49
|
+
Display-only: the server stays the source of truth."""
|
|
50
|
+
try:
|
|
51
|
+
with open(_cache_path()) as fh:
|
|
52
|
+
d = json.load(fh)
|
|
53
|
+
m = d.get("mode")
|
|
54
|
+
return m if m in VALID else None
|
|
55
|
+
except Exception:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def write_cached_mode(mode):
|
|
60
|
+
"""Persist the last-known server mode (None = server says unset). Best
|
|
61
|
+
effort, never raises: a cache miss only costs the boot display."""
|
|
62
|
+
try:
|
|
63
|
+
path = _cache_path()
|
|
64
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
65
|
+
tmp = path + ".tmp"
|
|
66
|
+
with open(tmp, "w") as fh:
|
|
67
|
+
json.dump({"mode": mode if mode in VALID else None, "at": time.time()}, fh)
|
|
68
|
+
os.replace(tmp, path)
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
31
73
|
def main() -> int:
|
|
32
74
|
argv = sys.argv[1:]
|
|
33
75
|
cmd = argv[0] if argv else "get"
|
|
34
76
|
try:
|
|
35
77
|
if cmd == "get":
|
|
36
|
-
|
|
37
|
-
|
|
78
|
+
try:
|
|
79
|
+
r = api_get("/api/v1/installations/posting-mode")
|
|
80
|
+
except Exception as e:
|
|
81
|
+
# Offline / server unreachable: answer from the last-known
|
|
82
|
+
# cache so UIs keep showing the user's chosen mode instead of
|
|
83
|
+
# silently falling back to "Steady". No "error" key: consumers
|
|
84
|
+
# treat that as a hard failure and drop the payload.
|
|
85
|
+
cached = read_cached_mode()
|
|
86
|
+
if cached is not None:
|
|
87
|
+
print(json.dumps({"mode": cached, "cached": True}))
|
|
88
|
+
return 0
|
|
89
|
+
raise e
|
|
90
|
+
d = (r or {}).get("data") or {}
|
|
91
|
+
if "mode" in d:
|
|
92
|
+
write_cached_mode(d.get("mode"))
|
|
93
|
+
print(json.dumps(d))
|
|
38
94
|
return 0
|
|
39
95
|
if cmd == "set":
|
|
40
96
|
if len(argv) < 2:
|
|
@@ -46,7 +102,10 @@ def main() -> int:
|
|
|
46
102
|
print(json.dumps({"error": f"invalid mode {raw!r}"}))
|
|
47
103
|
return 2
|
|
48
104
|
r = api_post("/api/v1/installations/posting-mode", {"mode": mode})
|
|
49
|
-
|
|
105
|
+
d = (r or {}).get("data") or {}
|
|
106
|
+
if "mode" in d:
|
|
107
|
+
write_cached_mode(d.get("mode"))
|
|
108
|
+
print(json.dumps(d))
|
|
50
109
|
return 0
|
|
51
110
|
print(json.dumps({"error": f"unknown command {cmd!r}"}))
|
|
52
111
|
return 2
|