@m13v/s4l 1.7.4 → 1.7.5-rc.1

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.
@@ -71,7 +71,7 @@ Boolean requesting whether a visible border and background is provided by the ho
71
71
  - "model": Tool visible to and callable by the agent
72
72
  - "app": Tool callable by the app from this server only`),csp:be().optional(),permissions:be().optional()});m({mimeTypes:z(d()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')});m({method:c("ui/download-file"),params:m({contents:z(P([Jc,Vc])).describe("Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types.")})});m({method:c("ui/message"),params:m({role:c("user").describe('Message role, currently only "user" is supported.'),content:z(mt).describe("Message content blocks (text, image, etc.).")})});m({method:c("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:Xi.optional().describe("CSP configuration from resource metadata."),permissions:eo.optional().describe("Sandbox permissions from resource metadata.")})});var qf=m({method:c("ui/notifications/tool-result"),params:Un.describe("Standard MCP tool execution result.")}),Yc=m({toolInfo:m({id:at.optional().describe("JSON-RPC id of the tools/call request."),tool:Yi.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:zf.optional().describe("Current color theme preference."),styles:Rf.optional().describe("Style configuration for theming the app."),displayMode:Je.optional().describe("How the UI is currently displayed."),availableDisplayModes:z(Je).optional().describe("Display modes the host supports."),containerDimensions:P([m({height:O().describe("Fixed container height in pixels.")}),m({maxHeight:P([O(),qe()]).optional().describe("Maximum container height in pixels.")})]).and(P([m({width:O().describe("Fixed container width in pixels.")}),m({maxWidth:P([O(),qe()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
73
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:P([c("web"),c("desktop"),c("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:m({touch:M().optional().describe("Whether the device supports touch input."),hover:M().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:m({top: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(),Hf=m({method:c("ui/notifications/host-context-changed"),params:Yc.describe("Partial context update containing only changed fields.")});m({method:c("ui/update-model-context"),params:m({content:z(mt).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:Z(d(),q().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});m({method:c("ui/initialize"),params:m({appInfo:On.describe("App identification (name and version)."),appCapabilities:Af.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var Ff=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:On.describe("Host application identification and version."),hostCapabilities:Cf.describe("Features and capabilities provided by the host."),hostContext:Yc.describe("Rich context about the host environment.")}).passthrough(),Jf={target:"draft-2020-12"};async function So(t,n){let r=t["~standard"];if(r.jsonSchema)return r.jsonSchema[n](Jf);if(r.vendor==="zod"){let{z:o}=await sl(()=>Promise.resolve().then(()=>Um),void 0,import.meta.url);return o.toJSONSchema(t,{io:n})}throw Error(`Schema (vendor: ${r.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function wo(t,n,r=""){let o=await t["~standard"].validate(n);if(o.issues){let e=o.issues.map(i=>{var s;let a=(s=i.path)==null?void 0:s.map(u=>typeof u=="object"?u.key:u).join(".");return a?`${a}: ${i.message}`:i.message}).join("; ");throw Error(r+e)}return o.value}function Vf(t){let n=document.documentElement;n.setAttribute("data-theme",t),n.style.colorScheme=t}function Bf(t,n=document.documentElement){for(let[r,o]of Object.entries(t))o!==void 0&&n.style.setProperty(r,o)}function Wf(t){if(document.getElementById("__mcp-host-fonts"))return;let n=document.createElement("style");n.id="__mcp-host-fonts",n.textContent=t,document.head.appendChild(n)}const xt=class xt extends kf{constructor(r,o={},e={autoResize:!0}){super(e);D(this,"_appInfo");D(this,"_capabilities");D(this,"options");D(this,"_hostCapabilities");D(this,"_hostInfo");D(this,"_hostContext");D(this,"_registeredTools",{});D(this,"_initializedSent",!1);D(this,"eventSchemas",{toolinput:Uf,toolinputpartial:Pf,toolresult:qf,toolcancelled:Ef,hostcontextchanged:Hf});D(this,"_everHadListener",new Set);D(this,"_toolHandlersInitialized",!1);D(this,"_onteardown");D(this,"_oncalltool");D(this,"_onlisttools");D(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=e,e.allowUnsafeEval||G({jitless:!0}),this.setRequestHandler(Tn,i=>(console.log("Received ping:",i.params),{})),this.setEventHandler("hostcontextchanged",void 0)}_assertInitialized(r){var e;if(this._initializedSent)return;let o=`[ext-apps] App.${r}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if((e=this.options)!=null&&e.strict)throw Error(o);console.warn(`${o}. This will throw in a future release.`)}_assertHandlerTiming(r){var e;if(!xt.ONE_SHOT_EVENTS.has(r)||this._everHadListener.has(r)||(this._everHadListener.add(r),!this._initializedSent))return;let o=`[ext-apps] "${String(r)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if((e=this.options)!=null&&e.strict)throw Error(o);console.warn(o)}setEventHandler(r,o){o&&this._assertHandlerTiming(r),super.setEventHandler(r,o)}addEventListener(r,o){this._assertHandlerTiming(r),super.addEventListener(r,o)}onEventDispatch(r,o){r==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...o})}registerCapabilities(r){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._capabilities=yf(this._capabilities,r)}registerTool(r,o,e){if(this._registeredTools[r])throw Error(`Tool ${r} is already registered`);let i=this,a=()=>{var p;i._initializedSent&&((p=i._capabilities.tools)!=null&&p.listChanged)&&i.sendToolListChanged()},s=o.inputSchema!==void 0,u={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]===u&&(delete i._registeredTools[r],a())},handler:async(p,b)=>{if(!u.enabled)throw Error(`Tool ${r} is disabled`);let g;if(s){let y=u.inputSchema,w=y?await wo(y,p??{},`Invalid input for tool ${r}: `):p??{};g=await e(w,b)}else g=await e(b);return u.outputSchema&&!g.isError&&(g.structuredContent=await wo(u.outputSchema,g.structuredContent,`Invalid output for tool ${r}: `)),g}};return this._registeredTools[r]=u,!this._capabilities.tools&&!this.transport&&this.registerCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),a(),u}ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(r,o)=>{let e=this._registeredTools[r.name];if(!e)throw Error(`Tool ${r.name} not found`);return e.handler(r.arguments,o)},this.onlisttools=async(r,o)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([e,i])=>i.enabled).map(async([e,i])=>{let a={name:e,title:i.title,description:i.description,inputSchema:i.inputSchema?await So(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await So(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(Zf,(o,e)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(o.params,e)})}get oncalltool(){return this._oncalltool}set oncalltool(r){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,r),this._oncalltool=r,this.replaceRequestHandler(Wc,(o,e)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(o.params,e)})}get onlisttools(){return this._onlisttools}set onlisttools(r){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,r),this._onlisttools=r,this.replaceRequestHandler(Bc,(o,e)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(o.params,e)})}assertCapabilityForMethod(r){var o;switch(r){case"sampling/createMessage":if(!((o=this._hostCapabilities)!=null&&o.sampling))throw Error(`Host does not support sampling (required for ${r})`);break}}assertRequestHandlerCapability(r){switch(r){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${r})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${r} registered`)}}assertNotificationCapability(r){}assertTaskCapability(r){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(r){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(r,o){if(this._assertInitialized("callServerTool"),typeof r=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${r}"). Did you mean: callServerTool({ name: "${r}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:r},Un,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},Fc,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},Hc,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let e=r.tools?Qc:Gc;return await this.request({method:"sampling/createMessage",params:r},e,o)}sendMessage(r,o){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:r},Tf,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},Zi,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},Nf,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},Of,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},Lf,o)}sendSizeChanged(r){return this.notification({method:"ui/notifications/size-changed",params:r})}setupSizeChangedNotifications(){let r=!1,o=0,e=0,i=()=>{r||(r=!0,requestAnimationFrame(()=>{r=!1;let s=document.documentElement,u=s.style.height;s.style.height="max-content";let p=Math.ceil(s.getBoundingClientRect().height);s.style.height=u;let b=Math.ceil(window.innerWidth);(b!==o||p!==e)&&(o=b,e=p,this.sendSizeChanged({width:b,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(r=new If(window.parent,window.parent),o){var e;if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1,await super.connect(r);try{let i=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:Sf}},Ff,o);if(i===void 0)throw Error(`Server sent invalid initialize result: ${i}`);this._hostCapabilities=i.hostCapabilities,this._hostInfo=i.hostInfo,this._hostContext=i.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,(e=this.options)!=null&&e.autoResize&&this.setupSizeChangedNotifications()}catch(i){throw this.close(),i}}};D(xt,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let Cn=xt;function Io(t){const n=t&&t.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(t&&t.content||[]).find(o=>o&&o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{}}return{}}class Kf{constructor(){D(this,"onhostcontextchanged");D(this,"onerror");D(this,"ontoolresult")}async connect(){var n,r;try{const[o,e]=await Promise.all([this.callServerTool({name:"project_config",arguments:{status:!0}}),this.callServerTool({name:"runtime",arguments:{action:"status"}})]),i=Io(o),a=Io(e),s=Array.isArray(i.projects)?i.projects:[],u={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(u)}})}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 Gf(){return globalThis.__S4L_BRIDGE__==="http"?new Kf:new Cn({name:"S4L Panel",version:"1.0.0"})}function Xc(t){const n=t.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(t.content||[]).find(o=>o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{_raw:r.text}}return{}}const Ze=t=>document.getElementById(t),Qf=Ze("pl-form"),An=Ze("pl-url"),wt=Ze("pl-submit"),zo=Ze("pl-sub"),Ln=Ze("pl-status"),Yf=Ze("pl-log"),$e=Gf();function el(t){var n,r,o;t.theme&&Vf(t.theme),(n=t.styles)!=null&&n.variables&&Bf(t.styles.variables),(o=(r=t.styles)==null?void 0:r.css)!=null&&o.fonts&&Wf(t.styles.css.fonts)}$e.onhostcontextchanged=el;$e.onerror=t=>console.error(t);let Ie=null,Mn=!0;function Pn(t){Yf.textContent=t}function ve(t){Ln.textContent=t,Ln.hidden=!1}function It(t,n){wt.disabled=t,n&&(wt.textContent=n)}function Xf(t){let n=t.trim();if(!n)return null;/^https?:\/\//i.test(n)||(n="https://"+n);try{return new URL(n).href}catch{return null}}function zt(t){const n=Array.isArray(t==null?void 0:t.projects)?t.projects:[];Ie=n.find(r=>(r.missing_required||[]).includes("website"))||n.find(r=>!r.ready)||n[0]||null,Mn=n.length===0,Mn||!Ie?(zo.textContent="Paste the link to your product to begin setup. S4L reads it to learn what to post about.",wt.textContent="Start setup"):(zo.textContent=`Set the product website for “${Ie.name}”. S4L re-reads it to keep posts on-message.`,wt.textContent=Ie.ready?"Update website":"Save website")}async function tl(){try{const t=await $e.callServerTool({name:"project_config",arguments:{status:!0}});zt(Xc(t))}catch(t){zt({projects:[]}),Pn("Couldn’t read current status: "+((t==null?void 0:t.message)||t))}}async function eh(t){It(!0,"Starting…");try{const n=await $e.sendMessage({role:"user",content:[{type:"text",text:`Set up social-autoposter for my product at ${t}. Research the site, infer a complete project (derive a short slug from the site), connect my X session, seed search topics, and continue setup end to end. Keep replies to me very concise.`}]});n!=null&&n.isError?ve("This host can’t start setup from the widget — paste your product link in the chat instead."):ve("Setup started. Watch the chat — S4L is researching your product now.")}catch(n){ve("Couldn’t start setup from here. Paste your product link in the chat instead."),Pn(String((n==null?void 0:n.message)||n))}finally{It(!1),zt({projects:[]})}}async function th(t,n){It(!0,"Saving…");try{const r=await $e.callServerTool({name:"project_config",arguments:{name:n.name,website:t}}),o=Xc(r);r!=null&&r.isError?ve("Save failed: "+((o==null?void 0:o._raw)||"see the chat for details.")):(ve(`Saved ${t} for “${n.name}”.`),An.value="",await tl())}catch(r){ve("Save failed: "+((r==null?void 0:r.message)||r))}finally{It(!1)}}Qf.addEventListener("submit",async t=>{t.preventDefault();const n=Xf(An.value);if(!n){ve("Enter a valid product URL, e.g. https://yourproduct.com"),An.focus();return}Ln.hidden=!0,Pn(""),Mn||!Ie?await eh(n):await th(n,Ie)});$e.connect().then(()=>{const t=$e.getHostContext();t&&el(t),tl()}).catch(t=>{Pn("Bridge connect failed: "+((t==null?void 0:t.message)||t)),zt({projects:[]})});</script>
74
- <style rel="stylesheet" crossorigin>:root{--bg: var(--background, #ffffff);--fg: var(--foreground, #141414);--muted: var(--muted-foreground, #6f6f6f);--faint: var(--muted-foreground, #9a9a9a);--fill: var(--card, #f6f6f6);--line: var(--border, #e4e4e4);--hairline: var(--border, #ececec);--btn-fg: var(--primary-foreground, #ffffff);--btn-bg: var(--primary, #141414);--mono: var(--font-mono, ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace)}@media(prefers-color-scheme:dark){:root{--bg: var(--background, #191919);--fg: var(--foreground, #f0f0f0);--muted: var(--muted-foreground, #9c9c9c);--faint: var(--muted-foreground, #6f6f6f);--fill: var(--card, #232323);--line: var(--border, #3a3a3a);--hairline: var(--border, #2d2d2d);--btn-fg: var(--primary-foreground, #141414);--btn-bg: var(--primary, #f0f0f0)}}*{box-sizing:border-box}html,body{margin:0;padding:0;background:var(--bg);color:var(--fg);font-family:var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);font-size:13px;line-height:1.5;-webkit-font-smoothing:antialiased}.wrap{max-width:620px;margin:0 auto;padding:18px 20px 24px;display:flex;flex-direction:column}.section-label,.settings-field>label,.settings-group-title,.stat .l{font-size:10.5px;font-weight:650;letter-spacing:.09em;text-transform:uppercase;color:var(--muted)}.ver,.setup-summary-count,.stat .n,.settings-project-state{font-family:var(--mono)}.head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-bottom:14px}.head-left{display:inline-flex;align-items:baseline;gap:10px;min-width:0}.head-right{display:inline-flex;align-items:center;gap:12px;flex:none}.head-pause{display:inline-flex;align-items:center;gap:7px}.head-pause-label{font-size:11.5px;font-weight:600;color:var(--muted)}.title{font-size:19px;font-weight:750;letter-spacing:-.03em}.ver{font-size:11px;color:var(--faint);display:inline-flex;align-items:center;gap:8px}.ver button.update-btn{padding:3px 10px;font-size:11.5px;font-weight:600;font-family:var(--font-sans, -apple-system, sans-serif);border-radius:999px;background:var(--btn-bg);color:var(--btn-fg);border:1px solid var(--btn-bg)}.setup-summary{display:inline-flex;align-items:center;gap:7px;flex:none;padding:5px 11px;font-size:11.5px;font-weight:600;color:var(--muted);background:transparent;border:1px solid var(--line);border-radius:999px}.setup-summary[hidden]{display:none}.setup-summary.complete{color:var(--fg)}.setup-summary-count{font-size:11px}.setup-summary-count[hidden]{display:none}.setup-summary.blocked{border-color:var(--fg);color:var(--fg)}.setup-summary.blocked .setup-summary-count{font-weight:700}.chevron{width:0;height:0;flex:none;border-left:3.5px solid transparent;border-right:3.5px solid transparent;border-top:4.5px solid currentColor;opacity:.7;transition:transform .16s ease}.expanded .chevron{transform:rotate(180deg)}button{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:1px solid var(--line);background:var(--fill);color:var(--fg);border-radius:8px;padding:6px 13px;font-size:12.5px;font-weight:600;font-family:inherit;line-height:1.3;white-space:nowrap;cursor:pointer;transition:border-color .12s ease,opacity .12s ease,background .12s ease}button:hover:not(:disabled){border-color:var(--fg)}button:disabled{opacity:.4;cursor:default}button.primary{background:var(--btn-bg);color:var(--btn-fg);border-color:var(--btn-bg)}button.primary:hover:not(:disabled){opacity:.85}button.ghost{background:transparent}button[hidden]{display:none}.actions{display:flex;flex-wrap:wrap;gap:8px}.actions:not(:empty){padding:2px 0 6px}.section{border-top:1px solid var(--hairline);padding:13px 0 15px;display:flex;flex-direction:column;gap:10px}.section[hidden]{display:none}.section-row{display:flex;align-items:center;justify-content:space-between;gap:8px 12px;width:100%;flex-wrap:wrap;min-height:24px}.section-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;align-items:center}button.section-row-toggle{background:none;border:none;padding:0;border-radius:0;font:inherit;color:inherit}button.section-row-toggle:hover:not(:disabled){opacity:1}button.section-row-toggle:hover .section-label{color:var(--fg)}.section-row-toggle .chevron{color:var(--muted)}.lane{display:flex;align-items:center;justify-content:space-between;gap:16px}.lane+.lane{margin-top:2px}.lane-text{min-width:0}.lane-name{font-size:13px;font-weight:600}.lane-desc{font-size:12px;color:var(--muted);margin-top:1px}.lane-note{font-size:12px;color:var(--muted)}.lane-note:empty{display:none}button.switch{flex:none;width:34px;height:20px;padding:2px;border:none;border-radius:999px;background:var(--line);display:inline-flex;align-items:center;transition:background .18s ease}button.switch:hover:not(:disabled){border:none}button.switch .knob{width:16px;height:16px;border-radius:50%;background:var(--bg);box-shadow:0 1px 2px #0000004d,0 0 0 1px #00000024;transform:translate(0);transition:transform .18s ease}button.switch[aria-checked=true]{background:var(--fg)}button.switch[aria-checked=true] .knob{transform:translate(14px)}button.switch:disabled{opacity:.45}.lane-split{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-top:2px}.lane-split[hidden]{display:none}.lane-split input[type=range]{flex:none;width:120px;height:20px;margin:0;-moz-appearance:none;appearance:none;-webkit-appearance:none;background:transparent}.lane-split input[type=range]::-webkit-slider-runnable-track{height:4px;border-radius:999px;background:var(--line)}.lane-split input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;margin-top:-6px;border-radius:50%;background:var(--bg);box-shadow:0 1px 2px #0000004d,0 0 0 1px #00000024}.lane-split input[type=range]:disabled{opacity:.45}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(76px,1fr));gap:0}.stats-grid[hidden]{display:none}.stat{padding:2px 14px 2px 0}.stat+.stat{border-left:1px solid var(--hairline);padding-left:14px}.stat .n{font-size:20px;font-weight:650;letter-spacing:-.02em;font-variant-numeric:tabular-nums}.stat .l{margin-top:2px}.settings-body{display:flex;flex-direction:column;gap:18px}.settings-body[hidden]{display:none}.settings-project{display:flex;flex-direction:column;gap:12px}.settings-project+.settings-project{border-top:1px solid var(--hairline);padding-top:16px}.settings-project-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.settings-project-name{font-size:14px;font-weight:700;letter-spacing:-.01em}.settings-project-tag{font-size:10px;font-weight:650;letter-spacing:.07em;text-transform:uppercase;color:var(--muted);border:1px solid var(--line);border-radius:999px;padding:2px 8px}.settings-project-state{font-size:11px;color:var(--muted);margin-left:auto}.settings-field{display:flex;flex-direction:column;gap:5px}.settings-input,.settings-textarea{width:100%;background:var(--fill);color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font:inherit;font-size:12.5px;line-height:1.5;transition:border-color .12s ease,background .12s ease}.settings-input:hover,.settings-textarea:hover{border-color:var(--muted)}.settings-input:focus,.settings-textarea:focus{outline:none;border-color:var(--fg);background:var(--bg)}.settings-input::placeholder,.settings-textarea::placeholder{color:var(--faint)}.settings-textarea{resize:vertical;min-height:36px}.settings-textarea.mono{font-family:var(--mono);font-size:11.5px;white-space:pre;overflow-x:auto;-moz-tab-size:2;tab-size:2}.settings-group{margin:2px 0 0;padding:2px 0 2px 14px;border:none;border-left:2px solid var(--hairline);display:flex;flex-direction:column;gap:10px;min-inline-size:0}.settings-group>legend{float:left;width:100%;padding:0;margin:0 0 2px;font-size:10.5px;font-weight:650;letter-spacing:.09em;text-transform:uppercase;color:var(--fg)}.settings-extra{font-size:11.5px;color:var(--faint)}.settings-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding-top:2px}.settings-status{font-size:12px;color:var(--muted)}.live-status{font-size:12px;color:var(--muted);min-height:0}.live-status:empty{display:none}.live-img{display:block;width:100%;border:1px solid var(--line);border-radius:8px;background:#000}.live-img[hidden]{display:none}.install,.menubar-banner{background:var(--fill);border:1px solid var(--line);border-radius:10px;padding:14px;margin-bottom:12px}.install{display:flex;flex-direction:column;gap:10px}.install[hidden]{display:none}.install-head{font-size:14px;font-weight:700}.install-desc{font-size:12px;color:var(--muted)}.install-actions{display:flex;gap:8px}.install-err{font-size:12px;color:var(--fg);font-weight:600;white-space:pre-wrap}.install-err[hidden]{display:none}.menubar-banner{display:flex;align-items:center;justify-content:space-between;gap:10px}.menubar-banner[hidden]{display:none}.menubar-banner-text{font-size:12.5px}.steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}.steps li{display:flex;align-items:baseline;gap:9px;font-size:12.5px;color:var(--muted)}.steps li .glyph{width:1.1em;flex:none;text-align:center;font-weight:700;font-family:var(--mono);font-size:11px;color:var(--fg)}.steps li.running,.steps li.done,.steps li.complete,.steps li.in_progress{color:var(--fg)}.steps li.pending{opacity:.5}.steps li.error,.steps li.blocked{color:var(--fg);font-weight:600}.steps li .detail{font-size:11px;color:var(--muted);font-weight:400}.onboarding-details{border-top:1px solid var(--hairline);padding:13px 0 15px;display:flex;flex-direction:column;gap:12px}.onboarding-details[hidden]{display:none}.onboarding-bar{height:3px;border-radius:999px;background:var(--line);overflow:hidden}.onboarding-bar>span{display:block;height:100%;width:0%;background:var(--fg);border-radius:inherit;transition:width .35s ease}.onboarding-steps{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px 16px}.onboarding-blocker{border-top:1px solid var(--hairline);padding-top:10px;color:var(--fg);font-size:12px;font-weight:600;white-space:pre-wrap}.onboarding-blocker[hidden]{display:none}@media(max-width:480px){.onboarding-steps{grid-template-columns:1fr}.wrap{padding:14px 14px 20px}}.log{font-size:11.5px;color:var(--muted);min-height:16px;white-space:pre-wrap;padding-top:8px}.muted{color:var(--muted)}.pl-wrap{max-width:460px;margin:0 auto;padding:18px 16px 24px;display:flex;flex-direction:column;gap:14px}.pl-head{display:flex;flex-direction:column;gap:4px}.pl-title{margin:0;font-size:17px;font-weight:650}.pl-sub{margin:0;font-size:13px;color:var(--muted)}.pl-muted{color:var(--muted);font-weight:400}.pl-form{display:flex;flex-direction:column;gap:8px}.pl-label{font-size:12px;font-weight:600;color:var(--muted)}.pl-input{width:100%;font:inherit;font-size:14px;padding:11px 12px;color:var(--fg);background:color-mix(in oklab,var(--fg) 5%,var(--bg));border:1.5px solid color-mix(in oklab,var(--fg) 34%,transparent);border-radius:9px;outline:none;transition:border-color .12s,box-shadow .12s,background .12s}.pl-input::placeholder{color:var(--muted)}.pl-input:hover{border-color:color-mix(in oklab,var(--fg) 50%,transparent)}.pl-input:focus{border-color:var(--fg);background:var(--bg);box-shadow:0 0 0 3px color-mix(in oklab,var(--fg) 14%,transparent)}.pl-btn{font:inherit;font-weight:560;padding:9px 14px;border-radius:8px;border:1px solid var(--btn-border);background:var(--bg);color:var(--fg);cursor:pointer;margin-top:4px}.pl-btn:hover{background:var(--card)}.pl-btn.primary{background:var(--btn-bg);color:var(--btn-fg);border-color:var(--btn-bg)}.pl-btn.primary:hover{opacity:.9}.pl-btn[disabled]{opacity:.5;cursor:default}.pl-status{font-size:13px;color:var(--fg);background:var(--card);border:1px solid var(--border);border-radius:8px;padding:9px 11px}.pl-log{font-size:12px;color:var(--muted);min-height:1em;white-space:pre-wrap}</style>
74
+ <style rel="stylesheet" crossorigin>:root{--bg: var(--background, #ffffff);--fg: var(--foreground, #141414);--muted: var(--muted-foreground, #6f6f6f);--faint: var(--muted-foreground, #9a9a9a);--fill: var(--card, #f6f6f6);--line: var(--border, #e4e4e4);--hairline: var(--border, #ececec);--btn-fg: var(--primary-foreground, #ffffff);--btn-bg: var(--primary, #141414);--mono: var(--font-mono, ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace)}@media(prefers-color-scheme:dark){:root{--bg: var(--background, #191919);--fg: var(--foreground, #f0f0f0);--muted: var(--muted-foreground, #9c9c9c);--faint: var(--muted-foreground, #6f6f6f);--fill: var(--card, #232323);--line: var(--border, #3a3a3a);--hairline: var(--border, #2d2d2d);--btn-fg: var(--primary-foreground, #141414);--btn-bg: var(--primary, #f0f0f0)}}*{box-sizing:border-box}html,body{margin:0;padding:0;background:var(--bg);color:var(--fg);font-family:var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);font-size:13px;line-height:1.5;-webkit-font-smoothing:antialiased}.wrap{max-width:620px;margin:0 auto;padding:18px 20px 24px;display:flex;flex-direction:column}.section-label,.settings-field>label,.settings-group-title,.stat .l{font-size:10.5px;font-weight:650;letter-spacing:.09em;text-transform:uppercase;color:var(--muted)}.ver,.setup-summary-count,.stat .n,.settings-project-state{font-family:var(--mono)}.head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-bottom:14px}.head-left{display:inline-flex;align-items:baseline;gap:10px;min-width:0}.head-right{display:inline-flex;align-items:center;gap:12px;flex:none}.head-pause{display:inline-flex;align-items:center;gap:7px}.head-pause-label{font-size:11.5px;font-weight:600;color:var(--muted)}.title{font-size:19px;font-weight:750;letter-spacing:-.03em}.ver{font-size:11px;color:var(--faint);display:inline-flex;align-items:center;gap:8px}.ver button.update-btn{padding:3px 10px;font-size:11.5px;font-weight:600;font-family:var(--font-sans, -apple-system, sans-serif);border-radius:999px;background:var(--btn-bg);color:var(--btn-fg);border:1px solid var(--btn-bg)}.setup-summary{display:inline-flex;align-items:center;gap:7px;flex:none;padding:5px 11px;font-size:11.5px;font-weight:600;color:var(--muted);background:transparent;border:1px solid var(--line);border-radius:999px}.setup-summary[hidden]{display:none}.setup-summary.complete{color:var(--fg)}.setup-summary-count{font-size:11px}.setup-summary-count[hidden]{display:none}.setup-summary.blocked{border-color:var(--fg);color:var(--fg)}.setup-summary.blocked .setup-summary-count{font-weight:700}.chevron{width:0;height:0;flex:none;border-left:3.5px solid transparent;border-right:3.5px solid transparent;border-top:4.5px solid currentColor;opacity:.7;transition:transform .16s ease}.expanded .chevron{transform:rotate(180deg)}button{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:1px solid var(--line);background:var(--fill);color:var(--fg);border-radius:8px;padding:6px 13px;font-size:12.5px;font-weight:600;font-family:inherit;line-height:1.3;white-space:nowrap;cursor:pointer;transition:border-color .12s ease,opacity .12s ease,background .12s ease}button:hover:not(:disabled){border-color:var(--fg)}button:disabled{opacity:.4;cursor:default}button.primary{background:var(--btn-bg);color:var(--btn-fg);border-color:var(--btn-bg)}button.primary:hover:not(:disabled){opacity:.85}button.ghost{background:transparent}button[hidden]{display:none}.actions{display:flex;flex-wrap:wrap;gap:8px}.actions:not(:empty){padding:2px 0 6px}.section{border-top:1px solid var(--hairline);padding:13px 0 15px;display:flex;flex-direction:column;gap:10px}.section[hidden]{display:none}.section-row{display:flex;align-items:center;justify-content:space-between;gap:8px 12px;width:100%;flex-wrap:wrap;min-height:24px}.section-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;align-items:center}button.section-row-toggle{background:none;border:none;padding:0;border-radius:0;font:inherit;color:inherit}button.section-row-toggle:hover:not(:disabled){opacity:1}button.section-row-toggle:hover .section-label{color:var(--fg)}.section-row-toggle .chevron{color:var(--muted)}.lane{display:flex;align-items:center;justify-content:space-between;gap:16px}.lane+.lane{margin-top:2px}.lane-text{min-width:0}.lane-name{font-size:13px;font-weight:600}.lane-desc{font-size:12px;color:var(--muted);margin-top:1px}.lane-note{font-size:12px;color:var(--muted)}.lane-note:empty{display:none}#posting-volume-select{flex:none;padding:4px 8px;font-size:12px;color:var(--fg);background:var(--fill);border:1px solid var(--line);border-radius:6px;cursor:pointer}#posting-volume-select:disabled{opacity:.5;cursor:default}button.switch{flex:none;width:34px;height:20px;padding:2px;border:none;border-radius:999px;background:var(--line);display:inline-flex;align-items:center;transition:background .18s ease}button.switch:hover:not(:disabled){border:none}button.switch .knob{width:16px;height:16px;border-radius:50%;background:var(--bg);box-shadow:0 1px 2px #0000004d,0 0 0 1px #00000024;transform:translate(0);transition:transform .18s ease}button.switch[aria-checked=true]{background:var(--fg)}button.switch[aria-checked=true] .knob{transform:translate(14px)}button.switch:disabled{opacity:.45}.lane-split{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-top:2px}.lane-split[hidden]{display:none}.lane-split input[type=range]{flex:none;width:120px;height:20px;margin:0;-moz-appearance:none;appearance:none;-webkit-appearance:none;background:transparent}.lane-split input[type=range]::-webkit-slider-runnable-track{height:4px;border-radius:999px;background:var(--line)}.lane-split input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;margin-top:-6px;border-radius:50%;background:var(--bg);box-shadow:0 1px 2px #0000004d,0 0 0 1px #00000024}.lane-split input[type=range]:disabled{opacity:.45}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(76px,1fr));gap:0}.stats-grid[hidden]{display:none}.stat{padding:2px 14px 2px 0}.stat+.stat{border-left:1px solid var(--hairline);padding-left:14px}.stat .n{font-size:20px;font-weight:650;letter-spacing:-.02em;font-variant-numeric:tabular-nums}.stat .l{margin-top:2px}.settings-body{display:flex;flex-direction:column;gap:18px}.settings-body[hidden]{display:none}.settings-project{display:flex;flex-direction:column;gap:12px}.settings-project+.settings-project{border-top:1px solid var(--hairline);padding-top:16px}.settings-project-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.settings-project-name{font-size:14px;font-weight:700;letter-spacing:-.01em}.settings-project-tag{font-size:10px;font-weight:650;letter-spacing:.07em;text-transform:uppercase;color:var(--muted);border:1px solid var(--line);border-radius:999px;padding:2px 8px}.settings-project-state{font-size:11px;color:var(--muted);margin-left:auto}.settings-field{display:flex;flex-direction:column;gap:5px}.settings-input,.settings-textarea{width:100%;background:var(--fill);color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font:inherit;font-size:12.5px;line-height:1.5;transition:border-color .12s ease,background .12s ease}.settings-input:hover,.settings-textarea:hover{border-color:var(--muted)}.settings-input:focus,.settings-textarea:focus{outline:none;border-color:var(--fg);background:var(--bg)}.settings-input::placeholder,.settings-textarea::placeholder{color:var(--faint)}.settings-textarea{resize:vertical;min-height:36px}.settings-textarea.mono{font-family:var(--mono);font-size:11.5px;white-space:pre;overflow-x:auto;-moz-tab-size:2;tab-size:2}.settings-group{margin:2px 0 0;padding:2px 0 2px 14px;border:none;border-left:2px solid var(--hairline);display:flex;flex-direction:column;gap:10px;min-inline-size:0}.settings-group>legend{float:left;width:100%;padding:0;margin:0 0 2px;font-size:10.5px;font-weight:650;letter-spacing:.09em;text-transform:uppercase;color:var(--fg)}.settings-extra{font-size:11.5px;color:var(--faint)}.settings-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding-top:2px}.settings-status{font-size:12px;color:var(--muted)}.live-status{font-size:12px;color:var(--muted);min-height:0}.live-status:empty{display:none}.live-img{display:block;width:100%;border:1px solid var(--line);border-radius:8px;background:#000}.live-img[hidden]{display:none}.install,.menubar-banner{background:var(--fill);border:1px solid var(--line);border-radius:10px;padding:14px;margin-bottom:12px}.install{display:flex;flex-direction:column;gap:10px}.install[hidden]{display:none}.install-head{font-size:14px;font-weight:700}.install-desc{font-size:12px;color:var(--muted)}.install-actions{display:flex;gap:8px}.install-err{font-size:12px;color:var(--fg);font-weight:600;white-space:pre-wrap}.install-err[hidden]{display:none}.menubar-banner{display:flex;align-items:center;justify-content:space-between;gap:10px}.menubar-banner[hidden]{display:none}.menubar-banner-text{font-size:12.5px}.steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}.steps li{display:flex;align-items:baseline;gap:9px;font-size:12.5px;color:var(--muted)}.steps li .glyph{width:1.1em;flex:none;text-align:center;font-weight:700;font-family:var(--mono);font-size:11px;color:var(--fg)}.steps li.running,.steps li.done,.steps li.complete,.steps li.in_progress{color:var(--fg)}.steps li.pending{opacity:.5}.steps li.error,.steps li.blocked{color:var(--fg);font-weight:600}.steps li .detail{font-size:11px;color:var(--muted);font-weight:400}.onboarding-details{border-top:1px solid var(--hairline);padding:13px 0 15px;display:flex;flex-direction:column;gap:12px}.onboarding-details[hidden]{display:none}.onboarding-bar{height:3px;border-radius:999px;background:var(--line);overflow:hidden}.onboarding-bar>span{display:block;height:100%;width:0%;background:var(--fg);border-radius:inherit;transition:width .35s ease}.onboarding-steps{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px 16px}.onboarding-blocker{border-top:1px solid var(--hairline);padding-top:10px;color:var(--fg);font-size:12px;font-weight:600;white-space:pre-wrap}.onboarding-blocker[hidden]{display:none}@media(max-width:480px){.onboarding-steps{grid-template-columns:1fr}.wrap{padding:14px 14px 20px}}.log{font-size:11.5px;color:var(--muted);min-height:16px;white-space:pre-wrap;padding-top:8px}.muted{color:var(--muted)}.pl-wrap{max-width:460px;margin:0 auto;padding:18px 16px 24px;display:flex;flex-direction:column;gap:14px}.pl-head{display:flex;flex-direction:column;gap:4px}.pl-title{margin:0;font-size:17px;font-weight:650}.pl-sub{margin:0;font-size:13px;color:var(--muted)}.pl-muted{color:var(--muted);font-weight:400}.pl-form{display:flex;flex-direction:column;gap:8px}.pl-label{font-size:12px;font-weight:600;color:var(--muted)}.pl-input{width:100%;font:inherit;font-size:14px;padding:11px 12px;color:var(--fg);background:color-mix(in oklab,var(--fg) 5%,var(--bg));border:1.5px solid color-mix(in oklab,var(--fg) 34%,transparent);border-radius:9px;outline:none;transition:border-color .12s,box-shadow .12s,background .12s}.pl-input::placeholder{color:var(--muted)}.pl-input:hover{border-color:color-mix(in oklab,var(--fg) 50%,transparent)}.pl-input:focus{border-color:var(--fg);background:var(--bg);box-shadow:0 0 0 3px color-mix(in oklab,var(--fg) 14%,transparent)}.pl-btn{font:inherit;font-weight:560;padding:9px 14px;border-radius:8px;border:1px solid var(--btn-border);background:var(--bg);color:var(--fg);cursor:pointer;margin-top:4px}.pl-btn:hover{background:var(--card)}.pl-btn.primary{background:var(--btn-bg);color:var(--btn-fg);border-color:var(--btn-bg)}.pl-btn.primary:hover{opacity:.9}.pl-btn[disabled]{opacity:.5;cursor:default}.pl-status{font-size:13px;color:var(--fg);background:var(--card);border:1px solid var(--border);border-radius:8px;padding:9px 11px}.pl-log{font-size:12px;color:var(--muted);min-height:1em;white-space:pre-wrap}</style>
75
75
  </head>
76
76
  <body>
77
77
  <main class="pl-wrap">
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.7.4",
3
- "installedAt": "2026-07-14T01:43:29.220Z"
2
+ "version": "1.7.5-rc.1",
3
+ "installedAt": "2026-07-14T02:19:33.570Z"
4
4
  }
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.4",
5
+ "version": "1.7.5-rc.1",
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": {
@@ -48,6 +48,10 @@
48
48
  "name": "restart_menubar",
49
49
  "description": "Restart the S4L menu bar app"
50
50
  },
51
+ {
52
+ "name": "posting_volume",
53
+ "description": "Read or set posting volume (high / medium / low)"
54
+ },
51
55
  {
52
56
  "name": "pause_s4l",
53
57
  "description": "Pause or resume S4L"
@@ -1150,6 +1150,17 @@ class S4LMenuBar(rumps.App):
1150
1150
  (14400, "Every 4 hours"),
1151
1151
  )
1152
1152
 
1153
+ # Posting volume (2026-07-13): server-side per-install throttle for the
1154
+ # twitter cycle's virality bar. None = driver default. The labels carry
1155
+ # ballpark rates; exact per-install estimates live on the dashboard's
1156
+ # Posting Volume section (Stats tab).
1157
+ POSTING_MODE_PRESETS = (
1158
+ (None, "Default (cycle setting)"),
1159
+ ("high", "High (100+ posts/day)"),
1160
+ ("medium", "Medium (~30 posts/day)"),
1161
+ ("low", "Low (~5 posts/day)"),
1162
+ )
1163
+
1153
1164
  def _cadence_label(self, secs=None):
1154
1165
  """Human label for a cadence value, for menu copy."""
1155
1166
  if secs is None:
@@ -1174,6 +1185,36 @@ class S4LMenuBar(rumps.App):
1174
1185
  sys.stderr.write(f"[s4l-menubar] cadence rebuild failed: {e}\n")
1175
1186
  sys.stderr.flush()
1176
1187
 
1188
+ def _posting_mode_label(self, mode):
1189
+ """Menu copy for a posting-volume mode value."""
1190
+ for preset, label in self.POSTING_MODE_PRESETS:
1191
+ if preset == mode:
1192
+ return label
1193
+ return "Default (cycle setting)"
1194
+
1195
+ def _on_posting_mode_preset(self, mode, _sender=None):
1196
+ """Set the server-side posting-volume mode (network POST; a click may
1197
+ take a beat). Failure keeps the old value and tells the user."""
1198
+ try:
1199
+ stored = st.write_posting_mode(mode)
1200
+ self._notify(
1201
+ "S4L posting volume",
1202
+ f"Posting volume: {self._posting_mode_label(stored)}. Applies from the next cycle.",
1203
+ )
1204
+ except Exception as e:
1205
+ sys.stderr.write(f"[s4l-menubar] posting-mode write failed: {e}\n")
1206
+ sys.stderr.flush()
1207
+ self._notify(
1208
+ "S4L posting volume",
1209
+ "Could not reach s4l.ai to change posting volume; try again.",
1210
+ )
1211
+ self._sig = None
1212
+ try:
1213
+ self._tick(None)
1214
+ except Exception as e:
1215
+ sys.stderr.write(f"[s4l-menubar] posting-mode rebuild failed: {e}\n")
1216
+ sys.stderr.flush()
1217
+
1177
1218
  def _set_split(self, share):
1178
1219
  """Set the personal-brand share (pure local mode.json write, same
1179
1220
  contract as _toggle_lane) and rebuild so the checkmark moves instantly."""
@@ -3527,6 +3568,23 @@ class S4LMenuBar(rumps.App):
3527
3568
  cadence_menu.add(it)
3528
3569
  items.append(cadence_menu)
3529
3570
 
3571
+ # Posting volume: server-side per-install throttle (virality bar).
3572
+ # read_posting_mode() is cached and non-blocking; until the first
3573
+ # refresh lands the submenu shows Default checked, then corrects on
3574
+ # the next rebuild.
3575
+ pv_mode = st.read_posting_mode()
3576
+ pv_menu = rumps.MenuItem(
3577
+ f"Posting volume: {self._posting_mode_label(pv_mode).split(' (')[0].lower()}"
3578
+ )
3579
+ for preset, label in self.POSTING_MODE_PRESETS:
3580
+ it = rumps.MenuItem(
3581
+ label,
3582
+ callback=functools.partial(self._on_posting_mode_preset, preset),
3583
+ )
3584
+ it.state = 1 if preset == pv_mode else 0
3585
+ pv_menu.add(it)
3586
+ items.append(pv_menu)
3587
+
3530
3588
  items.append(rumps.separator)
3531
3589
  items.append(rumps.MenuItem("Open dashboard", callback=self._open_dashboard))
3532
3590
  # The one entry point for overall feedback (the review card no longer
@@ -1291,6 +1291,78 @@ def write_reveal_cadence(secs):
1291
1291
  return secs
1292
1292
 
1293
1293
 
1294
+ # --- Posting volume mode (2026-07-13) ---------------------------------------
1295
+ # Server-side per-install throttle for the twitter cycle's virality bar:
1296
+ # high|medium|low|None (None = driver default). Source of truth is
1297
+ # installations.posting_mode via /api/v1/installations/posting-mode; unlike
1298
+ # the lane flags there is NO local mode.json copy, because the whole point is
1299
+ # remote adjustability (dashboard and menubar must agree with the server).
1300
+ # Reads are cached so the menu rebuild tick never blocks on the network: a
1301
+ # stale/missing cache kicks one background refresh and returns the last-known
1302
+ # value (None until the first refresh lands; the next rebuild shows the mark).
1303
+ POSTING_MODE_TTL_SECS = 600
1304
+ _posting_mode_cache = {"mode": None, "known": False, "at": 0.0}
1305
+ _posting_mode_lock = threading.Lock()
1306
+
1307
+
1308
+ def _scripts_on_path():
1309
+ repo = os.environ.get("S4L_REPO_DIR") or str(Path.home() / "social-autoposter")
1310
+ scripts = os.path.join(repo, "scripts")
1311
+ if scripts not in sys.path:
1312
+ sys.path.insert(0, scripts)
1313
+
1314
+
1315
+ def _refresh_posting_mode():
1316
+ try:
1317
+ _scripts_on_path()
1318
+ from http_api import api_get
1319
+
1320
+ r = api_get("/api/v1/installations/posting-mode")
1321
+ d = (r or {}).get("data") or {}
1322
+ mode = d.get("mode")
1323
+ with _posting_mode_lock:
1324
+ _posting_mode_cache.update(
1325
+ mode=mode if mode in ("high", "medium", "low") else None,
1326
+ known=True,
1327
+ at=time.time(),
1328
+ )
1329
+ except Exception as e:
1330
+ sys.stderr.write(f"[s4l-state] posting-mode refresh failed: {e}\n")
1331
+ with _posting_mode_lock:
1332
+ # Throttle retries to the TTL window; keep last-known value.
1333
+ _posting_mode_cache["at"] = time.time()
1334
+
1335
+
1336
+ def read_posting_mode():
1337
+ """Last-known posting mode ('high'|'medium'|'low') or None (default /
1338
+ unknown). Non-blocking: kicks a background refresh when stale."""
1339
+ with _posting_mode_lock:
1340
+ fresh = (time.time() - _posting_mode_cache["at"]) < POSTING_MODE_TTL_SECS
1341
+ mode = _posting_mode_cache["mode"]
1342
+ if not fresh:
1343
+ threading.Thread(target=_refresh_posting_mode, daemon=True).start()
1344
+ return mode
1345
+
1346
+
1347
+ def write_posting_mode(mode):
1348
+ """Set (or clear, mode=None) the server-side posting mode. Blocking POST;
1349
+ call from a menu click, not from the rebuild tick. Returns the stored
1350
+ mode on success; raises on network failure so the caller can notify."""
1351
+ _scripts_on_path()
1352
+ from http_api import api_post
1353
+
1354
+ r = api_post("/api/v1/installations/posting-mode", {"mode": mode})
1355
+ d = (r or {}).get("data") or {}
1356
+ stored = d.get("mode")
1357
+ with _posting_mode_lock:
1358
+ _posting_mode_cache.update(
1359
+ mode=stored if stored in ("high", "medium", "low") else None,
1360
+ known=True,
1361
+ at=time.time(),
1362
+ )
1363
+ return stored
1364
+
1365
+
1294
1366
  def write_mode(mode):
1295
1367
  """Legacy single-mode setter: named lane ON, the other OFF (compat)."""
1296
1368
  if mode not in _VALID_MODES:
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l-mcp",
3
- "version": "1.7.4",
3
+ "version": "1.7.5-rc.1",
4
4
  "private": true,
5
5
  "description": "Desktop MCP client for social-autoposter (X/Twitter rail): manual draft/review/approve loop, autopilot control, and stats. Thin wrapper over the existing pipeline scripts.",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l",
3
- "version": "1.7.4",
3
+ "version": "1.7.5-rc.1",
4
4
  "description": "Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Install as a Claude Code agent skill.",
5
5
  "bin": {
6
6
  "social-autoposter": "bin/cli.js",
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env python3
2
+ """Posting-volume mode CLI (2026-07-13).
3
+
4
+ Thin wrapper over /api/v1/installations/posting-mode for the MCP `posting_volume`
5
+ tool and the panel. The mode (high|medium|low|None) is stored SERVER-SIDE on
6
+ installations.posting_mode; the virality-threshold route maps it to a
7
+ percentile and overrides the cycle driver's request, so changes apply on the
8
+ install's next cycle with no client update. There is deliberately NO local
9
+ mode.json copy (dashboard, menubar, and panel must always agree with the
10
+ server).
11
+
12
+ Usage:
13
+ python3 scripts/s4l_posting_mode.py get
14
+ -> {"mode": ..., "rates": [{mode,pctile,threshold,est_posts_per_day}...],
15
+ "pool_count": N, "batch_count": N, "window_days": 7}
16
+ python3 scripts/s4l_posting_mode.py set <high|medium|low|default>
17
+ -> {"mode": ...} ("default" clears the override)
18
+
19
+ Prints JSON on stdout; exits non-zero with an {"error": ...} JSON on failure.
20
+ """
21
+ import json
22
+ import os
23
+ import sys
24
+
25
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
26
+ from http_api import api_get, api_post # noqa: E402
27
+
28
+ VALID = ("high", "medium", "low")
29
+
30
+
31
+ def main() -> int:
32
+ argv = sys.argv[1:]
33
+ cmd = argv[0] if argv else "get"
34
+ try:
35
+ if cmd == "get":
36
+ r = api_get("/api/v1/installations/posting-mode")
37
+ print(json.dumps((r or {}).get("data") or {}))
38
+ return 0
39
+ if cmd == "set":
40
+ if len(argv) < 2:
41
+ print(json.dumps({"error": "usage: set <high|medium|low|default>"}))
42
+ return 2
43
+ raw = argv[1].strip().lower()
44
+ mode = None if raw in ("default", "none", "null", "") else raw
45
+ if mode is not None and mode not in VALID:
46
+ print(json.dumps({"error": f"invalid mode {raw!r}"}))
47
+ return 2
48
+ r = api_post("/api/v1/installations/posting-mode", {"mode": mode})
49
+ print(json.dumps((r or {}).get("data") or {}))
50
+ return 0
51
+ print(json.dumps({"error": f"unknown command {cmd!r}"}))
52
+ return 2
53
+ except Exception as e: # network / API failure: JSON error, non-zero exit
54
+ print(json.dumps({"error": str(e)}))
55
+ return 1
56
+
57
+
58
+ if __name__ == "__main__":
59
+ sys.exit(main())