@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.
- package/mcp/dist/index.js +47 -0
- package/mcp/dist/panel.html +31 -16
- package/mcp/dist/product-link.html +1 -1
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +5 -1
- package/mcp/menubar/s4l_menubar.py +58 -0
- package/mcp/menubar/s4l_state.py +72 -0
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/s4l_posting_mode.py +59 -0
|
@@ -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">
|
package/mcp/dist/version.json
CHANGED
package/mcp/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"dxt_version": "0.1",
|
|
3
3
|
"name": "social-autoposter",
|
|
4
4
|
"display_name": "S4L",
|
|
5
|
-
"version": "1.7.
|
|
5
|
+
"version": "1.7.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
|
package/mcp/menubar/s4l_state.py
CHANGED
|
@@ -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.
|
|
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
|
@@ -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())
|