social-autoposter 1.6.147 → 1.6.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mcp/dist/index.js CHANGED
@@ -464,6 +464,14 @@ async function produceDrafts(project, onProgress) {
464
464
  await ensureOverlayWatch();
465
465
  let step = 0;
466
466
  let lastMsg = "";
467
+ // Granular scan progress for the menu-bar label. Phase 1 logs one
468
+ // `executing N queries` line (the total), then one `ok/err project=… kept=K`
469
+ // line per query. We count those to paint `scanning X · N/M · kept K` instead
470
+ // of a static "scanning X". Best-effort: missing total falls back to a plain
471
+ // count, and any parse miss just leaves the prior label up.
472
+ let scanTotal = 0;
473
+ let scanDone = 0;
474
+ let scanKept = 0;
467
475
  // ONE predictable, host-independent place to watch a draft_cycle run, so any
468
476
  // agent (or human) debugging "the cycle looks stuck" has an obvious path:
469
477
  // ~/social-autoposter/skill/logs/draft_cycle-mcp.log
@@ -505,6 +513,21 @@ async function produceDrafts(project, onProgress) {
505
513
  appendLog(`${t}\n`);
506
514
  console.error(`[draft_cycle] ${t}`);
507
515
  }
516
+ // Per-query scan progress -> granular menu-bar label. These lines only
517
+ // appear during Phase 1 (before 2b-prep), so they never fight the
518
+ // "drafting" label below.
519
+ let sm;
520
+ if ((sm = /executing (\d+) quer/.exec(t))) {
521
+ scanTotal = parseInt(sm[1], 10) || 0;
522
+ }
523
+ else if ((sm = /^\s*(?:ok|err)\s+project=/.exec(t))) {
524
+ scanDone += 1;
525
+ const km = /kept=(\d+)/.exec(t);
526
+ if (km)
527
+ scanKept += parseInt(km[1], 10) || 0;
528
+ const prog = scanTotal ? `${scanDone}/${scanTotal}` : `${scanDone}`;
529
+ writeActivity("scanning", `scanning X · ${prog} · kept ${scanKept}`);
530
+ }
508
531
  if (/Phase 2b-prep/.test(t))
509
532
  writeActivity("drafting", "drafting replies");
510
533
  if (!onProgress)
@@ -2683,6 +2706,28 @@ async function buildSnapshot() {
2683
2706
  if (schedule_state === "ok") {
2684
2707
  completeOnboardingMilestone("tasks_scheduled");
2685
2708
  }
2709
+ // ---- Live-truthful onboarding checklist -------------------------------------
2710
+ // The ledger (onboarding-progress.json) is append-only HISTORY: a completed
2711
+ // milestone never reverts, which is right for telemetry (attempts/blockers/
2712
+ // funnel) but WRONG as a live "is it set up now?" checklist. So for the
2713
+ // milestones that have a cheap live signal, overlay their CURRENT state for
2714
+ // DISPLAY (the dashboard + menu bar render this); the ledger file itself stays
2715
+ // untouched for telemetry. Milestones with no cheap live signal
2716
+ // (environment_checked, profile_scanned, topics_seeded) keep their ledger value.
2717
+ const liveMilestoneStatus = {
2718
+ runtime_ready: rtReady ? "complete" : "pending",
2719
+ x_connected: x.connected ? "complete" : "pending",
2720
+ mode_chosen: modeChosen() ? "complete" : "pending",
2721
+ project_ready: projects.some((p) => p.ready) ? "complete" : "pending",
2722
+ tasks_scheduled: schedule_state === "ok" ? "complete" : "pending",
2723
+ };
2724
+ const ledgerSnapshot = onboardingSnapshot();
2725
+ const liveMilestones = (ledgerSnapshot.milestones || []).map((m) => liveMilestoneStatus[m.id] ? { ...m, status: liveMilestoneStatus[m.id] } : m);
2726
+ const onboardingLive = {
2727
+ ...ledgerSnapshot,
2728
+ milestones: liveMilestones,
2729
+ complete: liveMilestones.every((m) => m.status === "complete"),
2730
+ };
2686
2731
  const snap = {
2687
2732
  projects,
2688
2733
  projects_total: projects.length,
@@ -2708,7 +2753,7 @@ async function buildSnapshot() {
2708
2753
  runtime_ready: rtReady,
2709
2754
  runtime_provisioning: isProvisioning(),
2710
2755
  setup_complete: setupComplete,
2711
- onboarding: onboardingSnapshot(),
2756
+ onboarding: onboardingLive,
2712
2757
  };
2713
2758
  // Persist this snapshot so the menu bar can answer "set up?" the SAME way when
2714
2759
  // the loopback server is unreachable (Claude Desktop closed or mid-restart)
@@ -70,7 +70,7 @@ Boolean requesting whether a visible border and background is provided by the ho
70
70
  - omitted: host decides border`)});m({method:u("ui/request-display-mode"),params:m({mode:et.describe("The display mode being requested.")})});var mh=m({mode:et.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),ph=U([u("model"),u("app")]).describe("Tool visibility scope - who can access the tool.");m({resourceUri:d().optional(),visibility:z(ph).optional().describe(`Who can access this tool. Default: ["model", "app"]
71
71
  - "model": Tool visible to and callable by the agent
72
72
  - "app": Tool callable by the app from this server only`),csp:Ie().optional(),permissions:Ie().optional()});m({mimeTypes:z(d()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')});m({method:u("ui/download-file"),params:m({contents:z(U([fl,hl])).describe("Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types.")})});m({method:u("ui/message"),params:m({role:u("user").describe('Message role, currently only "user" is supported.'),content:z(yt).describe("Message content blocks (text, image, etc.).")})});m({method:u("ui/notifications/sandbox-resource-ready"),params:m({html:d().describe("HTML content to load into the inner iframe."),sandbox:d().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:po.optional().describe("CSP configuration from resource metadata."),permissions:fo.optional().describe("Sandbox permissions from resource metadata.")})});var fh=m({method:u("ui/notifications/tool-result"),params:Fn.describe("Standard MCP tool execution result.")}),yl=m({toolInfo:m({id:ht.optional().describe("JSON-RPC id of the tools/call request."),tool:mo.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:Yf.optional().describe("Current color theme preference."),styles:ch.optional().describe("Style configuration for theming the app."),displayMode:et.optional().describe("How the UI is currently displayed."),availableDisplayModes:z(et).optional().describe("Display modes the host supports."),containerDimensions:U([m({height:T().describe("Fixed container height in pixels.")}),m({maxHeight:U([T(),Qe()]).optional().describe("Maximum container height in pixels.")})]).and(U([m({width:T().describe("Fixed container width in pixels.")}),m({maxWidth:U([T(),Qe()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
73
- container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:d().optional().describe("User's language and region preference in BCP 47 format."),timeZone:d().optional().describe("User's timezone in IANA format."),userAgent:d().optional().describe("Host application identifier."),platform:U([u("web"),u("desktop"),u("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:m({touch:M().optional().describe("Whether the device supports touch input."),hover:M().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:m({top:T().describe("Top safe area inset in pixels."),right:T().describe("Right safe area inset in pixels."),bottom:T().describe("Bottom safe area inset in pixels."),left:T().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),hh=m({method:u("ui/notifications/host-context-changed"),params:yl.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:z(yt).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:Z(d(),q().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});m({method:u("ui/initialize"),params:m({appInfo:qn.describe("App identification (name and version)."),appCapabilities:dh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var gh=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:qn.describe("Host application identification and version."),hostCapabilities:lh.describe("Features and capabilities provided by the host."),hostContext:yl.describe("Rich context about the host environment.")}).passthrough(),vh={target:"draft-2020-12"};async function Ho(t,n){let r=t["~standard"];if(r.jsonSchema)return r.jsonSchema[n](vh);if(r.vendor==="zod"){let{z:o}=await Ul(()=>Promise.resolve().then(()=>ip),void 0,import.meta.url);return o.toJSONSchema(t,{io:n})}throw Error(`Schema (vendor: ${r.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function Fo(t,n,r=""){let o=await t["~standard"].validate(n);if(o.issues){let e=o.issues.map(i=>{var s;let a=(s=i.path)==null?void 0:s.map(c=>typeof c=="object"?c.key:c).join(".");return a?`${a}: ${i.message}`:i.message}).join("; ");throw Error(r+e)}return o.value}function _h(t){let n=document.documentElement;n.setAttribute("data-theme",t),n.style.colorScheme=t}function bh(t,n=document.documentElement){for(let[r,o]of Object.entries(t))o!==void 0&&n.style.setProperty(r,o)}function $h(t){if(document.getElementById("__mcp-host-fonts"))return;let n=document.createElement("style");n.id="__mcp-host-fonts",n.textContent=t,document.head.appendChild(n)}const At=class At extends Wf{constructor(r,o={},e={autoResize:!0}){super(e);D(this,"_appInfo");D(this,"_capabilities");D(this,"options");D(this,"_hostCapabilities");D(this,"_hostInfo");D(this,"_hostContext");D(this,"_registeredTools",{});D(this,"_initializedSent",!1);D(this,"eventSchemas",{toolinput:ih,toolinputpartial:oh,toolresult:fh,toolcancelled:ah,hostcontextchanged:hh});D(this,"_everHadListener",new Set);D(this,"_toolHandlersInitialized",!1);D(this,"_onteardown");D(this,"_oncalltool");D(this,"_onlisttools");D(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=e,e.allowUnsafeEval||X({jitless:!0}),this.setRequestHandler(Hn,i=>(console.log("Received ping:",i.params),{})),this.setEventHandler("hostcontextchanged",void 0)}_assertInitialized(r){var e;if(this._initializedSent)return;let o=`[ext-apps] App.${r}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if((e=this.options)!=null&&e.strict)throw Error(o);console.warn(`${o}. This will throw in a future release.`)}_assertHandlerTiming(r){var e;if(!At.ONE_SHOT_EVENTS.has(r)||this._everHadListener.has(r)||(this._everHadListener.add(r),!this._initializedSent))return;let o=`[ext-apps] "${String(r)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if((e=this.options)!=null&&e.strict)throw Error(o);console.warn(o)}setEventHandler(r,o){o&&this._assertHandlerTiming(r),super.setEventHandler(r,o)}addEventListener(r,o){this._assertHandlerTiming(r),super.addEventListener(r,o)}onEventDispatch(r,o){r==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...o})}registerCapabilities(r){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._capabilities=Bf(this._capabilities,r)}registerTool(r,o,e){if(this._registeredTools[r])throw Error(`Tool ${r} is already registered`);let i=this,a=()=>{var p;i._initializedSent&&((p=i._capabilities.tools)!=null&&p.listChanged)&&i.sendToolListChanged()},s=o.inputSchema!==void 0,c={title:o.title,description:o.description,inputSchema:o.inputSchema,outputSchema:o.outputSchema,annotations:o.annotations,_meta:o._meta,enabled:!0,enable(){this.enabled=!0,a()},disable(){this.enabled=!1,a()},update(p){Object.assign(this,p),a()},remove(){i._registeredTools[r]===c&&(delete i._registeredTools[r],a())},handler:async(p,b)=>{if(!c.enabled)throw Error(`Tool ${r} is disabled`);let g;if(s){let y=c.inputSchema,w=y?await Fo(y,p??{},`Invalid input for tool ${r}: `):p??{};g=await e(w,b)}else g=await e(b);return c.outputSchema&&!g.isError&&(g.structuredContent=await Fo(c.outputSchema,g.structuredContent,`Invalid output for tool ${r}: `)),g}};return this._registeredTools[r]=c,!this._capabilities.tools&&!this.transport&&this.registerCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),a(),c}ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(r,o)=>{let e=this._registeredTools[r.name];if(!e)throw Error(`Tool ${r.name} not found`);return e.handler(r.arguments,o)},this.onlisttools=async(r,o)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([e,i])=>i.enabled).map(async([e,i])=>{let a={name:e,title:i.title,description:i.description,inputSchema:i.inputSchema?await Ho(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await Ho(i.outputSchema,"output")),i.annotations&&(a.annotations=i.annotations),i._meta&&(a._meta=i._meta),a}))}))}async sendToolListChanged(r={}){this._assertInitialized("sendToolListChanged"),await this.notification({method:"notifications/tools/list_changed",params:r})}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}get ontoolinput(){return this.getEventHandler("toolinput")}set ontoolinput(r){this.setEventHandler("toolinput",r)}get ontoolinputpartial(){return this.getEventHandler("toolinputpartial")}set ontoolinputpartial(r){this.setEventHandler("toolinputpartial",r)}get ontoolresult(){return this.getEventHandler("toolresult")}set ontoolresult(r){this.setEventHandler("toolresult",r)}get ontoolcancelled(){return this.getEventHandler("toolcancelled")}set ontoolcancelled(r){this.setEventHandler("toolcancelled",r)}get onhostcontextchanged(){return this.getEventHandler("hostcontextchanged")}set onhostcontextchanged(r){this.setEventHandler("hostcontextchanged",r)}get onteardown(){return this._onteardown}set onteardown(r){this.warnIfRequestHandlerReplaced("onteardown",this._onteardown,r),this._onteardown=r,this.replaceRequestHandler(uh,(o,e)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(o.params,e)})}get oncalltool(){return this._oncalltool}set oncalltool(r){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,r),this._oncalltool=r,this.replaceRequestHandler(vl,(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(gl,(o,e)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(o.params,e)})}assertCapabilityForMethod(r){var o;switch(r){case"sampling/createMessage":if(!((o=this._hostCapabilities)!=null&&o.sampling))throw Error(`Host does not support sampling (required for ${r})`);break}}assertRequestHandlerCapability(r){switch(r){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${r})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${r} registered`)}}assertNotificationCapability(r){}assertTaskCapability(r){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(r){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(r,o){if(this._assertInitialized("callServerTool"),typeof r=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${r}"). Did you mean: callServerTool({ name: "${r}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:r},Fn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},pl,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},ml,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let e=r.tools?$l:bl;return await this.request({method:"sampling/createMessage",params:r},e,o)}sendMessage(r,o){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:r},rh,o)}sendLog(r){return this.notification({method:"notifications/message",params:r})}updateModelContext(r,o){return this._assertInitialized("updateModelContext"),this.request({method:"ui/update-model-context",params:r},Gi,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},th,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},nh,o)}requestTeardown(r={}){return this.notification({method:"ui/notifications/request-teardown",params:r})}requestDisplayMode(r,o){return this._assertInitialized("requestDisplayMode"),this.request({method:"ui/request-display-mode",params:r},mh,o)}sendSizeChanged(r){return this.notification({method:"ui/notifications/size-changed",params:r})}setupSizeChangedNotifications(){let r=!1,o=0,e=0,i=()=>{r||(r=!0,requestAnimationFrame(()=>{r=!1;let s=document.documentElement,c=s.style.height;s.style.height="max-content";let p=Math.ceil(s.getBoundingClientRect().height);s.style.height=c;let b=Math.ceil(window.innerWidth);(b!==o||p!==e)&&(o=b,e=p,this.sendSizeChanged({width:b,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(r=new Qf(window.parent,window.parent),o){var e;if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1,await super.connect(r);try{let i=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:Kf}},gh,o);if(i===void 0)throw Error(`Server sent invalid initialize result: ${i}`);this._hostCapabilities=i.hostCapabilities,this._hostInfo=i.hostInfo,this._hostContext=i.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,(e=this.options)!=null&&e.autoResize&&this.setupSizeChangedNotifications()}catch(i){throw this.close(),i}}};D(At,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let er=At;function Jo(t){const n=t&&t.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(t&&t.content||[]).find(o=>o&&o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{}}return{}}class yh{constructor(){D(this,"onhostcontextchanged");D(this,"onerror");D(this,"ontoolresult")}async connect(){var n,r;try{const[o,e]=await Promise.all([this.callServerTool({name:"project_config",arguments:{status:!0}}),this.callServerTool({name:"runtime",arguments:{action:"status"}})]),i=Jo(o),a=Jo(e),s=Array.isArray(i.projects)?i.projects:[],c={projects:s,projects_total:s.length,projects_ready:s.filter(p=>p&&p.ready).length,x_connected:!!i.x_connected,x_state:i.x_state||"",x_handle:i.x_handle??null,version:i.mcp_version||"",latest_version:i.latest_version??null,update_available:!!i.update_available,runtime_ready:typeof a.runtime_ready=="boolean"?a.runtime_ready:!0,runtime_provisioning:!!a.provisioning,onboarding:a.onboarding||i.onboarding};(n=this.ontoolresult)==null||n.call(this,{structuredContent:{snapshot:JSON.stringify(c)}})}catch(o){(r=this.onerror)==null||r.call(this,o)}}getHostContext(){}async callServerTool(n){const r=await fetch(`/tool/${encodeURIComponent(n.name)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n.arguments??{})});if(!r.ok){let o=`HTTP ${r.status}`;try{o=await r.text()||o}catch{}return{isError:!0,content:[{type:"text",text:o}]}}return r.json()}async sendMessage(){return{isError:!0}}}function kh(){return globalThis.__SAPS_BRIDGE__==="http"?new yh:new er({name:"S4L Panel",version:"1.0.0"})}function kl(t){const n=t.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(t.content||[]).find(o=>o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{_raw:r.text}}return{}}const V=t=>document.getElementById(t),Sl=V("ver"),Be=V("btn-setup"),Rt=V("btn-schedule"),It=V("stats-grid"),Jn=V("stats-toggle"),Sh=V("log"),wh=V("install-card"),Se=V("setup-summary"),wl=V("onboarding-details"),Ih=V("onboarding-steps"),Vn=V("onboarding-blocker"),Vo=V("onboarding-count"),zh=V("onboarding-bar-fill"),xh=V("live-card"),jh=V("stats-card"),Bo=V("install-steps"),Oe=V("install-err"),he=V("btn-install"),ho=V("btn-live"),go=V("btn-live-stop"),Wo=V("btn-live-front"),Pe=V("live-status"),Zt=V("live-img");let B=null,Bn=!1,zt=!1,Wn=!1,Ue=!1,Je=!1;function Q(t){Sh.textContent=t}function Nh(t){switch(t){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function vo(t){if(!t||!Array.isArray(t.steps)){Bo.innerHTML="";return}Bo.innerHTML=t.steps.map(n=>{const r=n.detail&&n.status!=="pending"?` <span class="detail">${n.status==="error"?n.detail:""}</span>`:"";return`<li class="${n.status}"><span class="glyph">${Nh(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),t.error?(Oe.textContent=t.error,Oe.hidden=!1):Oe.hidden=!0}const Th={environment_checked:"Environment checked",runtime_ready:"Runtime ready",x_connected:"X connected",profile_scanned:"Profile scanned",project_ready:"Project ready",topics_seeded:"Topics seeded",tasks_scheduled:"Autopilot scheduled"};function Oh(t){switch(t){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function Il(){wl.hidden=!Ue,Se.setAttribute("aria-expanded",String(Ue)),Se.classList.toggle("expanded",Ue)}function Ph(t){if(!t||!Array.isArray(t.milestones)){Se.hidden=!0,wl.hidden=!0;return}Se.hidden=!1;const n=t.milestones.length,r=t.milestones.filter(e=>e.status==="complete").length,o=!!t.current_blocker&&!t.complete;Se.classList.toggle("complete",t.complete),Se.classList.toggle("blocked",o),Vo.hidden=t.complete,Vo.textContent=o?`${r}/${n} · needs you`:zt?`${r}/${n} · setting up…`:`${r}/${n}`,zh.style.width=n>0?`${Math.round(r/n*100)}%`:"0%",Ih.innerHTML=t.milestones.map(e=>{const i=Th[e.id]||e.id,a=e.attempts>1?` <span class="detail">${e.attempts} attempts</span>`:"";return`<li class="${e.status}"><span class="glyph">${Oh(e.status)}</span><span>${i}${a}</span></li>`}).join(""),t.current_blocker?(Vn.textContent=`Current blocker: ${t.current_blocker.message}`,Vn.hidden=!1,Ue=!0):Vn.hidden=!0,Il()}function Ct(){if(!B)return;Ph(B.onboarding),Sl.innerHTML=B.update_available&&B.latest_version?`v${B.version} · <button id="btn-update" class="update-btn">Update to ${B.latest_version}</button>`:`v${B.version}`;const t=!B.runtime_ready;wh.hidden=!t;const n=B.projects_ready>0,r=!t&&n&&B.x_connected;Be.hidden=r,Be.disabled=!1,Be.classList.toggle("primary",!r);const o=r&&(B.schedule_state==="missing"||B.schedule_state==="disabled");Rt.hidden=!o,Rt.classList.toggle("primary",o),xh.hidden=!r,jh.hidden=!r}function Me(t){B={...B||{},...t},Ct()}function Uh(t){const n=Array.isArray(t.projects)?t.projects:[];return{projects:n,projects_total:n.length,projects_ready:n.filter(r=>r.ready).length,x_connected:!!t.x_connected,x_state:t.x_state||"",x_handle:t.x_handle??null,version:t.mcp_version||(B==null?void 0:B.version)||"",latest_version:t.latest_version??null,update_available:!!t.update_available,onboarding:t.onboarding}}const be=kh();function zl(t){var n,r,o;t.theme&&_h(t.theme),(n=t.styles)!=null&&n.variables&&bh(t.styles.variables),(o=(r=t.styles)==null?void 0:r.css)!=null&&o.fonts&&$h(t.styles.css.fonts)}be.onhostcontextchanged=zl;be.onerror=t=>console.error(t);be.ontoolresult=t=>{const n=kl(t);n&&typeof n.projects_total=="number"&&(Me(n),n.runtime_ready?xl():n.runtime_provisioning&&bo())};async function fe(t,n={}){const r=await be.callServerTool({name:t,arguments:n});return kl(r)}async function _o(){Q("Refreshing…");try{const[t,n]=await Promise.all([fe("project_config",{status:!0}),fe("runtime",{action:"status"}).catch(()=>({}))]);Me({...Uh(t),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},onboarding:n.onboarding||t.onboarding||(B==null?void 0:B.onboarding)}),B&&!B.runtime_ready&&n.provisioning&&bo(),Q(""),xl()}catch(t){Q("Refresh failed: "+((t==null?void 0:t.message)||t))}}async function bo(){if(!Bn){Bn=!0,he.disabled=!0,he.textContent="Installing…";try{for(;;){const t=await fe("runtime",{action:"status"}).catch(()=>({}));if(vo(t.progress??null),t.onboarding&&Me({onboarding:t.onboarding}),t.runtime_ready){Me({runtime_ready:!0}),Q("Runtime installed; you're ready to set up."),_o();return}const n=t.progress??null;if(n&&n.done&&!n.ok){he.disabled=!1,he.textContent="Retry install",Q("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{Bn=!1}}}async function Eh(){var r;if(zt)return;zt=!0,Ct();const t=Date.now(),n=1200*1e3;try{for(;;){const o=await fe("runtime",{action:"status"}).catch(()=>({}));o.progress&&vo(o.progress);const e={};if(typeof o.runtime_ready=="boolean"&&(e.runtime_ready=o.runtime_ready),o.onboarding&&(e.onboarding=o.onboarding),Object.keys(e).length&&Me(e),(r=o.onboarding)!=null&&r.complete){await _o(),Q("Setup complete.");break}if(Date.now()-t>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{zt=!1,Ct()}}async function xl(){try{const t=await fe("get_stats",{days:7}),n=Array.isArray(t.projects)?t.projects[0]:null,r=n==null?void 0:n.posts;if(!r){It.innerHTML='<div class="muted">No stats yet.</div>';return}const o=[["Posts",r.total??0],["Active",r.active??0],["Views",r.views_period_total??r.views??0],["Replies",r.comments_period_total??r.comments??0],["Clicks",r.post_clicks_period_total??0]];It.innerHTML=o.map(([e,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${e}</div></div>`).join("")}catch(t){It.innerHTML=`<div class="muted">Stats unavailable: ${(t==null?void 0:t.message)||t}</div>`}}function $o(t,n,r){const o=t.textContent;t.disabled=!0,t.textContent=n,r().finally(()=>{t.disabled=!1,t.textContent=o,Ct()})}Be.addEventListener("click",()=>$o(Be,"Starting…",async()=>{Q("Asking Claude to run setup…");try{const t=await be.sendMessage({role:"user",content:[{type:"text",text:"Set up S4L end to end now. Inspect and repair the runtime, auto-detect and connect my X session, scan my profile, discover and research my product, then infer and save a complete project with seeded search topics. Keep going without asking me to approve each safe setup step. Ask only if I must interactively sign in or no product can be identified. Keep every reply to me extremely concise: a few short sentences at most, no step-by-step narration or long status walls. If you must ask me something (e.g. the product URL), make it one short question."}]});t!=null&&t.isError?Q("The host rejected the setup request — type “set up S4L” in the chat instead."):(Q("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),Eh())}catch(t){Q("Couldn’t start setup: "+((t==null?void 0:t.message)||t))}}));Rt.addEventListener("click",()=>$o(Rt,"Setting up…",async()=>{Q("Asking Claude to schedule the draft tasks for this account…");try{const t=await be.sendMessage({role:"user",content:[{type:"text",text:'Set up the social-autoposter draft autopilot schedule for this Claude account. If queue_setup is available, call it; then for EACH of saps-phase1-query and saps-phase2b-draft call the host tool create_scheduled_task with taskId, cronExpression "* * * * *", and the prompt — read it from ~/.claude/scheduled-tasks/<taskId>/SKILL.md (already on disk). Do NOT redo my X connection or project setup. Keep replies to me very short.'}]});t!=null&&t.isError?Q("The host rejected it — type “set up the draft schedule” in the chat instead."):Q("Scheduling is running in the chat. The draft tasks will register for this account.")}catch(t){Q("Couldn’t start scheduling: "+((t==null?void 0:t.message)||t))}}));Se.addEventListener("click",()=>{Ue=!Ue,Il()});Jn.addEventListener("click",()=>{Je=!Je,It.hidden=!Je,Jn.setAttribute("aria-expanded",String(Je)),Jn.classList.toggle("expanded",Je)});Sl.addEventListener("click",t=>{const n=t.target;n&&n.id==="btn-update"&&Dh()});async function Dh(){if(Wn)return;Wn=!0;const t=document.getElementById("btn-update");t&&(t.disabled=!0,t.textContent="Updating…"),Q("Installing the latest release… this can take a minute.");try{const n=await fe("runtime",{action:"update"});n.ok?(Q(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),t&&(t.textContent="Update installed — restart to apply")):(Q("Update failed (exit "+(n.exit_code??"?")+"). Try `npx social-autoposter@latest update` in a terminal."),t&&(t.disabled=!1,t.textContent="Retry update"))}catch(n){Q("Update failed: "+((n==null?void 0:n.message)||n)),t&&(t.disabled=!1,t.textContent="Retry update")}finally{Wn=!1}}he.addEventListener("click",async()=>{Oe.hidden=!0,he.disabled=!0,he.textContent="Starting…",Q("Installing the runtime — this is a one-time download (~150MB+).");try{const t=await fe("runtime",{action:"install"});if(t.runtime_ready){Me({runtime_ready:!0}),_o();return}vo(t.progress??null),bo()}catch(t){he.disabled=!1,he.textContent="Retry install",Oe.textContent="Couldn't start install: "+((t==null?void 0:t.message)||t),Oe.hidden=!1}});let xt=null,Kn=!1;async function Ko(){if(!Kn){Kn=!0;try{const t=await fe("show_browser_to_user",{action:"frame"});if(!t.ok){Pe.textContent=t.message||"No active browser session.",yo(!1);return}t.frame&&(Zt.src=t.frame,Zt.hidden=!1);const n=t.title||t.url||(t.port?"port "+t.port:"");Pe.textContent=t.frame?"Watching"+(n?": "+n:""):"Connecting…"}catch(t){Pe.textContent="Live view error: "+((t==null?void 0:t.message)||t)}finally{Kn=!1}}}function Rh(){ho.hidden=!0,go.hidden=!1,Pe.textContent="Attaching to the browser…",Ko(),xt=setInterval(Ko,450)}function yo(t=!0){xt!=null&&(clearInterval(xt),xt=null),ho.hidden=!1,go.hidden=!0,Zt.hidden=!0,Zt.removeAttribute("src"),t&&fe("show_browser_to_user",{action:"stop"}).catch(()=>{})}ho.addEventListener("click",Rh);go.addEventListener("click",()=>{yo(!0),Pe.textContent=""});Wo.addEventListener("click",()=>$o(Wo,"Bringing…",async()=>{yo(!0);const t=await fe("show_browser_to_user",{action:"front"});Pe.textContent=t!=null&&t.ok?"Brought the browser to the front.":(t==null?void 0:t.message)||"Couldn't bring the browser to the front."}));be.connect().then(()=>{const t=be.getHostContext();t&&zl(t)});</script>
73
+ container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:d().optional().describe("User's language and region preference in BCP 47 format."),timeZone:d().optional().describe("User's timezone in IANA format."),userAgent:d().optional().describe("Host application identifier."),platform:U([u("web"),u("desktop"),u("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:m({touch:M().optional().describe("Whether the device supports touch input."),hover:M().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:m({top:T().describe("Top safe area inset in pixels."),right:T().describe("Right safe area inset in pixels."),bottom:T().describe("Bottom safe area inset in pixels."),left:T().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),hh=m({method:u("ui/notifications/host-context-changed"),params:yl.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:z(yt).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:Z(d(),q().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});m({method:u("ui/initialize"),params:m({appInfo:qn.describe("App identification (name and version)."),appCapabilities:dh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var gh=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:qn.describe("Host application identification and version."),hostCapabilities:lh.describe("Features and capabilities provided by the host."),hostContext:yl.describe("Rich context about the host environment.")}).passthrough(),vh={target:"draft-2020-12"};async function Ho(t,n){let r=t["~standard"];if(r.jsonSchema)return r.jsonSchema[n](vh);if(r.vendor==="zod"){let{z:o}=await Ul(()=>Promise.resolve().then(()=>ip),void 0,import.meta.url);return o.toJSONSchema(t,{io:n})}throw Error(`Schema (vendor: ${r.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function Fo(t,n,r=""){let o=await t["~standard"].validate(n);if(o.issues){let e=o.issues.map(i=>{var s;let a=(s=i.path)==null?void 0:s.map(c=>typeof c=="object"?c.key:c).join(".");return a?`${a}: ${i.message}`:i.message}).join("; ");throw Error(r+e)}return o.value}function _h(t){let n=document.documentElement;n.setAttribute("data-theme",t),n.style.colorScheme=t}function bh(t,n=document.documentElement){for(let[r,o]of Object.entries(t))o!==void 0&&n.style.setProperty(r,o)}function $h(t){if(document.getElementById("__mcp-host-fonts"))return;let n=document.createElement("style");n.id="__mcp-host-fonts",n.textContent=t,document.head.appendChild(n)}const At=class At extends Wf{constructor(r,o={},e={autoResize:!0}){super(e);D(this,"_appInfo");D(this,"_capabilities");D(this,"options");D(this,"_hostCapabilities");D(this,"_hostInfo");D(this,"_hostContext");D(this,"_registeredTools",{});D(this,"_initializedSent",!1);D(this,"eventSchemas",{toolinput:ih,toolinputpartial:oh,toolresult:fh,toolcancelled:ah,hostcontextchanged:hh});D(this,"_everHadListener",new Set);D(this,"_toolHandlersInitialized",!1);D(this,"_onteardown");D(this,"_oncalltool");D(this,"_onlisttools");D(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=e,e.allowUnsafeEval||X({jitless:!0}),this.setRequestHandler(Hn,i=>(console.log("Received ping:",i.params),{})),this.setEventHandler("hostcontextchanged",void 0)}_assertInitialized(r){var e;if(this._initializedSent)return;let o=`[ext-apps] App.${r}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if((e=this.options)!=null&&e.strict)throw Error(o);console.warn(`${o}. This will throw in a future release.`)}_assertHandlerTiming(r){var e;if(!At.ONE_SHOT_EVENTS.has(r)||this._everHadListener.has(r)||(this._everHadListener.add(r),!this._initializedSent))return;let o=`[ext-apps] "${String(r)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if((e=this.options)!=null&&e.strict)throw Error(o);console.warn(o)}setEventHandler(r,o){o&&this._assertHandlerTiming(r),super.setEventHandler(r,o)}addEventListener(r,o){this._assertHandlerTiming(r),super.addEventListener(r,o)}onEventDispatch(r,o){r==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...o})}registerCapabilities(r){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._capabilities=Bf(this._capabilities,r)}registerTool(r,o,e){if(this._registeredTools[r])throw Error(`Tool ${r} is already registered`);let i=this,a=()=>{var p;i._initializedSent&&((p=i._capabilities.tools)!=null&&p.listChanged)&&i.sendToolListChanged()},s=o.inputSchema!==void 0,c={title:o.title,description:o.description,inputSchema:o.inputSchema,outputSchema:o.outputSchema,annotations:o.annotations,_meta:o._meta,enabled:!0,enable(){this.enabled=!0,a()},disable(){this.enabled=!1,a()},update(p){Object.assign(this,p),a()},remove(){i._registeredTools[r]===c&&(delete i._registeredTools[r],a())},handler:async(p,b)=>{if(!c.enabled)throw Error(`Tool ${r} is disabled`);let g;if(s){let y=c.inputSchema,w=y?await Fo(y,p??{},`Invalid input for tool ${r}: `):p??{};g=await e(w,b)}else g=await e(b);return c.outputSchema&&!g.isError&&(g.structuredContent=await Fo(c.outputSchema,g.structuredContent,`Invalid output for tool ${r}: `)),g}};return this._registeredTools[r]=c,!this._capabilities.tools&&!this.transport&&this.registerCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),a(),c}ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(r,o)=>{let e=this._registeredTools[r.name];if(!e)throw Error(`Tool ${r.name} not found`);return e.handler(r.arguments,o)},this.onlisttools=async(r,o)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([e,i])=>i.enabled).map(async([e,i])=>{let a={name:e,title:i.title,description:i.description,inputSchema:i.inputSchema?await Ho(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await Ho(i.outputSchema,"output")),i.annotations&&(a.annotations=i.annotations),i._meta&&(a._meta=i._meta),a}))}))}async sendToolListChanged(r={}){this._assertInitialized("sendToolListChanged"),await this.notification({method:"notifications/tools/list_changed",params:r})}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}get ontoolinput(){return this.getEventHandler("toolinput")}set ontoolinput(r){this.setEventHandler("toolinput",r)}get ontoolinputpartial(){return this.getEventHandler("toolinputpartial")}set ontoolinputpartial(r){this.setEventHandler("toolinputpartial",r)}get ontoolresult(){return this.getEventHandler("toolresult")}set ontoolresult(r){this.setEventHandler("toolresult",r)}get ontoolcancelled(){return this.getEventHandler("toolcancelled")}set ontoolcancelled(r){this.setEventHandler("toolcancelled",r)}get onhostcontextchanged(){return this.getEventHandler("hostcontextchanged")}set onhostcontextchanged(r){this.setEventHandler("hostcontextchanged",r)}get onteardown(){return this._onteardown}set onteardown(r){this.warnIfRequestHandlerReplaced("onteardown",this._onteardown,r),this._onteardown=r,this.replaceRequestHandler(uh,(o,e)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(o.params,e)})}get oncalltool(){return this._oncalltool}set oncalltool(r){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,r),this._oncalltool=r,this.replaceRequestHandler(vl,(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(gl,(o,e)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(o.params,e)})}assertCapabilityForMethod(r){var o;switch(r){case"sampling/createMessage":if(!((o=this._hostCapabilities)!=null&&o.sampling))throw Error(`Host does not support sampling (required for ${r})`);break}}assertRequestHandlerCapability(r){switch(r){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${r})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${r} registered`)}}assertNotificationCapability(r){}assertTaskCapability(r){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(r){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(r,o){if(this._assertInitialized("callServerTool"),typeof r=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${r}"). Did you mean: callServerTool({ name: "${r}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:r},Fn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},pl,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},ml,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let e=r.tools?$l:bl;return await this.request({method:"sampling/createMessage",params:r},e,o)}sendMessage(r,o){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:r},rh,o)}sendLog(r){return this.notification({method:"notifications/message",params:r})}updateModelContext(r,o){return this._assertInitialized("updateModelContext"),this.request({method:"ui/update-model-context",params:r},Gi,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},th,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},nh,o)}requestTeardown(r={}){return this.notification({method:"ui/notifications/request-teardown",params:r})}requestDisplayMode(r,o){return this._assertInitialized("requestDisplayMode"),this.request({method:"ui/request-display-mode",params:r},mh,o)}sendSizeChanged(r){return this.notification({method:"ui/notifications/size-changed",params:r})}setupSizeChangedNotifications(){let r=!1,o=0,e=0,i=()=>{r||(r=!0,requestAnimationFrame(()=>{r=!1;let s=document.documentElement,c=s.style.height;s.style.height="max-content";let p=Math.ceil(s.getBoundingClientRect().height);s.style.height=c;let b=Math.ceil(window.innerWidth);(b!==o||p!==e)&&(o=b,e=p,this.sendSizeChanged({width:b,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(r=new Qf(window.parent,window.parent),o){var e;if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1,await super.connect(r);try{let i=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:Kf}},gh,o);if(i===void 0)throw Error(`Server sent invalid initialize result: ${i}`);this._hostCapabilities=i.hostCapabilities,this._hostInfo=i.hostInfo,this._hostContext=i.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,(e=this.options)!=null&&e.autoResize&&this.setupSizeChangedNotifications()}catch(i){throw this.close(),i}}};D(At,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let er=At;function Jo(t){const n=t&&t.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(t&&t.content||[]).find(o=>o&&o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{}}return{}}class yh{constructor(){D(this,"onhostcontextchanged");D(this,"onerror");D(this,"ontoolresult")}async connect(){var n,r;try{const[o,e]=await Promise.all([this.callServerTool({name:"project_config",arguments:{status:!0}}),this.callServerTool({name:"runtime",arguments:{action:"status"}})]),i=Jo(o),a=Jo(e),s=Array.isArray(i.projects)?i.projects:[],c={projects:s,projects_total:s.length,projects_ready:s.filter(p=>p&&p.ready).length,x_connected:!!i.x_connected,x_state:i.x_state||"",x_handle:i.x_handle??null,version:i.mcp_version||"",latest_version:i.latest_version??null,update_available:!!i.update_available,runtime_ready:typeof a.runtime_ready=="boolean"?a.runtime_ready:!0,runtime_provisioning:!!a.provisioning,onboarding:a.onboarding||i.onboarding};(n=this.ontoolresult)==null||n.call(this,{structuredContent:{snapshot:JSON.stringify(c)}})}catch(o){(r=this.onerror)==null||r.call(this,o)}}getHostContext(){}async callServerTool(n){const r=await fetch(`/tool/${encodeURIComponent(n.name)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n.arguments??{})});if(!r.ok){let o=`HTTP ${r.status}`;try{o=await r.text()||o}catch{}return{isError:!0,content:[{type:"text",text:o}]}}return r.json()}async sendMessage(){return{isError:!0}}}function kh(){return globalThis.__SAPS_BRIDGE__==="http"?new yh:new er({name:"S4L Panel",version:"1.0.0"})}function kl(t){const n=t.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(t.content||[]).find(o=>o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{_raw:r.text}}return{}}const V=t=>document.getElementById(t),Sl=V("ver"),Be=V("btn-setup"),Rt=V("btn-schedule"),It=V("stats-grid"),Jn=V("stats-toggle"),Sh=V("log"),wh=V("install-card"),Se=V("setup-summary"),wl=V("onboarding-details"),Ih=V("onboarding-steps"),Vn=V("onboarding-blocker"),Vo=V("onboarding-count"),zh=V("onboarding-bar-fill"),xh=V("live-card"),jh=V("stats-card"),Bo=V("install-steps"),Oe=V("install-err"),he=V("btn-install"),ho=V("btn-live"),go=V("btn-live-stop"),Wo=V("btn-live-front"),Pe=V("live-status"),Zt=V("live-img");let B=null,Bn=!1,zt=!1,Wn=!1,Ue=!1,Je=!1;function Q(t){Sh.textContent=t}function Nh(t){switch(t){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function vo(t){if(!t||!Array.isArray(t.steps)){Bo.innerHTML="";return}Bo.innerHTML=t.steps.map(n=>{const r=n.detail&&n.status!=="pending"?` <span class="detail">${n.status==="error"?n.detail:""}</span>`:"";return`<li class="${n.status}"><span class="glyph">${Nh(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),t.error?(Oe.textContent=t.error,Oe.hidden=!1):Oe.hidden=!0}const Th={environment_checked:"Environment checked",runtime_ready:"Runtime ready",x_connected:"X connected",profile_scanned:"Profile scanned",project_ready:"Project ready",topics_seeded:"Topics seeded",tasks_scheduled:"Tasks scheduled"};function Oh(t){switch(t){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function Il(){wl.hidden=!Ue,Se.setAttribute("aria-expanded",String(Ue)),Se.classList.toggle("expanded",Ue)}function Ph(t){if(!t||!Array.isArray(t.milestones)){Se.hidden=!0,wl.hidden=!0;return}Se.hidden=!1;const n=t.milestones.length,r=t.milestones.filter(e=>e.status==="complete").length,o=!!t.current_blocker&&!t.complete;Se.classList.toggle("complete",t.complete),Se.classList.toggle("blocked",o),Vo.hidden=t.complete,Vo.textContent=o?`${r}/${n} · needs you`:zt?`${r}/${n} · setting up…`:`${r}/${n}`,zh.style.width=n>0?`${Math.round(r/n*100)}%`:"0%",Ih.innerHTML=t.milestones.map(e=>{const i=Th[e.id]||e.id,a=e.attempts>1?` <span class="detail">${e.attempts} attempts</span>`:"";return`<li class="${e.status}"><span class="glyph">${Oh(e.status)}</span><span>${i}${a}</span></li>`}).join(""),t.current_blocker?(Vn.textContent=`Current blocker: ${t.current_blocker.message}`,Vn.hidden=!1,Ue=!0):Vn.hidden=!0,Il()}function Ct(){if(!B)return;Ph(B.onboarding),Sl.innerHTML=B.update_available&&B.latest_version?`v${B.version} · <button id="btn-update" class="update-btn">Update to ${B.latest_version}</button>`:`v${B.version}`;const t=!B.runtime_ready;wh.hidden=!t;const n=B.projects_ready>0,r=!t&&n&&B.x_connected;Be.hidden=r,Be.disabled=!1,Be.classList.toggle("primary",!r);const o=r&&(B.schedule_state==="missing"||B.schedule_state==="disabled");Rt.hidden=!o,Rt.classList.toggle("primary",o),xh.hidden=!r,jh.hidden=!r}function Me(t){B={...B||{},...t},Ct()}function Uh(t){const n=Array.isArray(t.projects)?t.projects:[];return{projects:n,projects_total:n.length,projects_ready:n.filter(r=>r.ready).length,x_connected:!!t.x_connected,x_state:t.x_state||"",x_handle:t.x_handle??null,version:t.mcp_version||(B==null?void 0:B.version)||"",latest_version:t.latest_version??null,update_available:!!t.update_available,onboarding:t.onboarding}}const be=kh();function zl(t){var n,r,o;t.theme&&_h(t.theme),(n=t.styles)!=null&&n.variables&&bh(t.styles.variables),(o=(r=t.styles)==null?void 0:r.css)!=null&&o.fonts&&$h(t.styles.css.fonts)}be.onhostcontextchanged=zl;be.onerror=t=>console.error(t);be.ontoolresult=t=>{const n=kl(t);n&&typeof n.projects_total=="number"&&(Me(n),n.runtime_ready?xl():n.runtime_provisioning&&bo())};async function fe(t,n={}){const r=await be.callServerTool({name:t,arguments:n});return kl(r)}async function _o(){Q("Refreshing…");try{const[t,n]=await Promise.all([fe("project_config",{status:!0}),fe("runtime",{action:"status"}).catch(()=>({}))]);Me({...Uh(t),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},onboarding:n.onboarding||t.onboarding||(B==null?void 0:B.onboarding)}),B&&!B.runtime_ready&&n.provisioning&&bo(),Q(""),xl()}catch(t){Q("Refresh failed: "+((t==null?void 0:t.message)||t))}}async function bo(){if(!Bn){Bn=!0,he.disabled=!0,he.textContent="Installing…";try{for(;;){const t=await fe("runtime",{action:"status"}).catch(()=>({}));if(vo(t.progress??null),t.onboarding&&Me({onboarding:t.onboarding}),t.runtime_ready){Me({runtime_ready:!0}),Q("Runtime installed; you're ready to set up."),_o();return}const n=t.progress??null;if(n&&n.done&&!n.ok){he.disabled=!1,he.textContent="Retry install",Q("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{Bn=!1}}}async function Eh(){var r;if(zt)return;zt=!0,Ct();const t=Date.now(),n=1200*1e3;try{for(;;){const o=await fe("runtime",{action:"status"}).catch(()=>({}));o.progress&&vo(o.progress);const e={};if(typeof o.runtime_ready=="boolean"&&(e.runtime_ready=o.runtime_ready),o.onboarding&&(e.onboarding=o.onboarding),Object.keys(e).length&&Me(e),(r=o.onboarding)!=null&&r.complete){await _o(),Q("Setup complete.");break}if(Date.now()-t>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{zt=!1,Ct()}}async function xl(){try{const t=await fe("get_stats",{days:7}),n=Array.isArray(t.projects)?t.projects[0]:null,r=n==null?void 0:n.posts;if(!r){It.innerHTML='<div class="muted">No stats yet.</div>';return}const o=[["Posts",r.total??0],["Active",r.active??0],["Views",r.views_period_total??r.views??0],["Replies",r.comments_period_total??r.comments??0],["Clicks",r.post_clicks_period_total??0]];It.innerHTML=o.map(([e,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${e}</div></div>`).join("")}catch(t){It.innerHTML=`<div class="muted">Stats unavailable: ${(t==null?void 0:t.message)||t}</div>`}}function $o(t,n,r){const o=t.textContent;t.disabled=!0,t.textContent=n,r().finally(()=>{t.disabled=!1,t.textContent=o,Ct()})}Be.addEventListener("click",()=>$o(Be,"Starting…",async()=>{Q("Asking Claude to run setup…");try{const t=await be.sendMessage({role:"user",content:[{type:"text",text:"Set up S4L end to end now. Inspect and repair the runtime, auto-detect and connect my X session, scan my profile, discover and research my product, then infer and save a complete project with seeded search topics. Keep going without asking me to approve each safe setup step. Ask only if I must interactively sign in or no product can be identified. Keep every reply to me extremely concise: a few short sentences at most, no step-by-step narration or long status walls. If you must ask me something (e.g. the product URL), make it one short question."}]});t!=null&&t.isError?Q("The host rejected the setup request — type “set up S4L” in the chat instead."):(Q("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),Eh())}catch(t){Q("Couldn’t start setup: "+((t==null?void 0:t.message)||t))}}));Rt.addEventListener("click",()=>$o(Rt,"Setting up…",async()=>{Q("Asking Claude to schedule the draft tasks for this account…");try{const t=await be.sendMessage({role:"user",content:[{type:"text",text:'Set up the social-autoposter draft autopilot schedule for this Claude account. If queue_setup is available, call it; then for EACH of saps-phase1-query and saps-phase2b-draft call the host tool create_scheduled_task with taskId, cronExpression "* * * * *", and the prompt — read it from ~/.claude/scheduled-tasks/<taskId>/SKILL.md (already on disk). Do NOT redo my X connection or project setup. Keep replies to me very short.'}]});t!=null&&t.isError?Q("The host rejected it — type “set up the draft schedule” in the chat instead."):Q("Scheduling is running in the chat. The draft tasks will register for this account.")}catch(t){Q("Couldn’t start scheduling: "+((t==null?void 0:t.message)||t))}}));Se.addEventListener("click",()=>{Ue=!Ue,Il()});Jn.addEventListener("click",()=>{Je=!Je,It.hidden=!Je,Jn.setAttribute("aria-expanded",String(Je)),Jn.classList.toggle("expanded",Je)});Sl.addEventListener("click",t=>{const n=t.target;n&&n.id==="btn-update"&&Dh()});async function Dh(){if(Wn)return;Wn=!0;const t=document.getElementById("btn-update");t&&(t.disabled=!0,t.textContent="Updating…"),Q("Installing the latest release… this can take a minute.");try{const n=await fe("runtime",{action:"update"});n.ok?(Q(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),t&&(t.textContent="Update installed — restart to apply")):(Q("Update failed (exit "+(n.exit_code??"?")+"). Try `npx social-autoposter@latest update` in a terminal."),t&&(t.disabled=!1,t.textContent="Retry update"))}catch(n){Q("Update failed: "+((n==null?void 0:n.message)||n)),t&&(t.disabled=!1,t.textContent="Retry update")}finally{Wn=!1}}he.addEventListener("click",async()=>{Oe.hidden=!0,he.disabled=!0,he.textContent="Starting…",Q("Installing the runtime — this is a one-time download (~150MB+).");try{const t=await fe("runtime",{action:"install"});if(t.runtime_ready){Me({runtime_ready:!0}),_o();return}vo(t.progress??null),bo()}catch(t){he.disabled=!1,he.textContent="Retry install",Oe.textContent="Couldn't start install: "+((t==null?void 0:t.message)||t),Oe.hidden=!1}});let xt=null,Kn=!1;async function Ko(){if(!Kn){Kn=!0;try{const t=await fe("show_browser_to_user",{action:"frame"});if(!t.ok){Pe.textContent=t.message||"No active browser session.",yo(!1);return}t.frame&&(Zt.src=t.frame,Zt.hidden=!1);const n=t.title||t.url||(t.port?"port "+t.port:"");Pe.textContent=t.frame?"Watching"+(n?": "+n:""):"Connecting…"}catch(t){Pe.textContent="Live view error: "+((t==null?void 0:t.message)||t)}finally{Kn=!1}}}function Rh(){ho.hidden=!0,go.hidden=!1,Pe.textContent="Attaching to the browser…",Ko(),xt=setInterval(Ko,450)}function yo(t=!0){xt!=null&&(clearInterval(xt),xt=null),ho.hidden=!1,go.hidden=!0,Zt.hidden=!0,Zt.removeAttribute("src"),t&&fe("show_browser_to_user",{action:"stop"}).catch(()=>{})}ho.addEventListener("click",Rh);go.addEventListener("click",()=>{yo(!0),Pe.textContent=""});Wo.addEventListener("click",()=>$o(Wo,"Bringing…",async()=>{yo(!0);const t=await fe("show_browser_to_user",{action:"front"});Pe.textContent=t!=null&&t.ok?"Brought the browser to the front.":(t==null?void 0:t.message)||"Couldn't bring the browser to the front."}));be.connect().then(()=>{const t=be.getHostContext();t&&zl(t)});</script>
74
74
  <style rel="stylesheet" crossorigin>:root{--bg: var(--background, #ffffff);--fg: var(--foreground, #111111);--muted: var(--muted-foreground, #6b6b6b);--card: var(--card, #f5f5f5);--border: var(--border, #e2e2e2);--btn-fg: var(--primary-foreground, #ffffff);--btn-bg: var(--primary, #111111);--btn-border: var(--border, #c4c4c4)}@media(prefers-color-scheme:dark){:root{--bg: var(--background, #1a1a1a);--fg: var(--foreground, #f2f2f2);--muted: var(--muted-foreground, #9a9a9a);--card: var(--card, #262626);--border: var(--border, #3a3a3a);--btn-fg: var(--primary-foreground, #111111);--btn-bg: var(--primary, #f2f2f2);--btn-border: var(--border, #555555)}}*{box-sizing:border-box}html,body{margin:0;padding:0;background:var(--bg);color:var(--fg);font-family:var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);font-size:14px;line-height:1.4}.wrap{max-width:640px;margin:0 auto;padding:14px;display:flex;flex-direction:column;gap:10px}.head{display:flex;align-items:center;justify-content:space-between;gap:8px}.head-left{display:inline-flex;align-items:baseline;gap:8px;min-width:0}.title{font-size:16px;font-weight:650;letter-spacing:-.01em}.ver{font-size:12px;color:var(--muted);display:inline-flex;align-items:center;gap:6px}.ver button.update-btn{padding:3px 10px;font-size:12px;font-weight:600;border-radius:7px;background:var(--btn-bg);color:var(--btn-fg);border:1px solid var(--btn-bg)}.setup-summary{display:inline-flex;align-items:center;gap:6px;flex:none;padding:5px 10px;font-size:12px;font-weight:550;color:var(--muted);background:var(--bg);border:1px solid var(--border);border-radius:8px}.setup-summary[hidden]{display:none}.setup-summary.complete{color:var(--fg)}.setup-summary-label{font-weight:600}.setup-summary-count{color:var(--muted)}.setup-summary-count[hidden]{display:none}.setup-summary.blocked .setup-summary-count{color:var(--fg);font-weight:600}.chevron{width:0;height:0;flex:none;border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid currentColor;transition:transform .15s ease}.expanded .chevron{transform:rotate(180deg)}button{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:1px solid var(--btn-border);background:var(--card);color:var(--fg);border-radius:8px;padding:7px 12px;font-size:13px;font-weight:550;line-height:1.2;white-space:nowrap;cursor:pointer;transition:opacity .12s ease}button:hover:not(:disabled){opacity:.82}button:disabled{opacity:.45;cursor:default}button.primary{background:var(--btn-bg);color:var(--btn-fg);border-color:var(--btn-bg)}button.ghost{background:var(--bg)}button[hidden]{display:none}.actions{display:flex;flex-wrap:wrap;gap:8px}.section{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:8px}.section[hidden]{display:none}.section-row{display:flex;align-items:center;justify-content:space-between;gap:8px 12px;width:100%;flex-wrap:wrap}.section-label{font-size:13px;font-weight:600;color:var(--fg)}.section-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}button.section-row-toggle{background:none;border:none;padding:0;border-radius:0;font:inherit;color:inherit}button.section-row-toggle:hover:not(:disabled){opacity:1}.section-row-toggle .chevron{color:var(--muted)}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(88px,1fr));gap:8px}.stats-grid[hidden]{display:none}.stat{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:8px 10px}.stat .n{font-size:17px;font-weight:650}.stat .l{font-size:11px;color:var(--muted);margin-top:1px}.config-desc{font-size:12px;color:var(--muted)}.config-desc[hidden]{display:none}.config-editor{width:100%;min-height:260px;resize:vertical;background:var(--bg);color:var(--fg);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-family:var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);font-size:12px;line-height:1.45;-moz-tab-size:2;tab-size:2;white-space:pre;overflow:auto}.config-editor[hidden]{display:none}.config-status{font-size:11px;color:var(--muted);white-space:pre-wrap;word-break:break-all}.live-status{font-size:11px;color:var(--muted);min-height:14px}.live-img{display:block;width:100%;border:1px solid var(--border);border-radius:6px;background:#000}.live-img[hidden]{display:none}.install{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:12px;display:flex;flex-direction:column;gap:10px}.install[hidden]{display:none}.install-head{font-size:14px;font-weight:650}.install-desc{font-size:12px;color:var(--muted)}.install-actions{display:flex;gap:8px}.install-err{font-size:12px;color:var(--fg);font-weight:600;white-space:pre-wrap}.install-err[hidden]{display:none}.steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}.steps li{display:flex;align-items:baseline;gap:8px;font-size:13px;color:var(--muted)}.steps li .glyph{width:1.1em;flex:none;text-align:center;font-weight:650;color:var(--fg)}.steps li.running,.steps li.done,.steps li.complete,.steps li.in_progress{color:var(--fg)}.steps li.pending{opacity:.55}.steps li.error,.steps li.blocked{color:var(--fg);font-weight:600}.steps li .detail{font-size:11px;color:var(--muted)}.onboarding-details{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:12px;display:flex;flex-direction:column;gap:10px}.onboarding-details[hidden]{display:none}.onboarding-bar{height:4px;border-radius:999px;background:var(--border);overflow:hidden}.onboarding-bar>span{display:block;height:100%;width:0%;background:var(--fg);border-radius:inherit;transition:width .35s ease}.onboarding-steps{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px 12px}.onboarding-blocker{border-top:1px solid var(--border);padding-top:9px;color:var(--fg);font-size:12px;font-weight:600;white-space:pre-wrap}.onboarding-blocker[hidden]{display:none}@media(max-width:520px){.onboarding-steps{grid-template-columns:1fr}}.log{font-size:12px;color:var(--muted);min-height:16px;white-space:pre-wrap}.muted{color:var(--muted)}</style>
75
75
  </head>
76
76
  <body>
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.6.147",
3
- "installedAt": "2026-06-29T21:43:27.026Z"
2
+ "version": "1.6.149",
3
+ "installedAt": "2026-06-29T22:07:06.183Z"
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.6.147",
5
+ "version": "1.6.149",
6
6
  "description": "Draft, review, approve, and autopilot X/Twitter posts.",
7
7
  "long_description": "The disclaimer above is generic Claude boilerplate. S4L 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 end to end**\n\n2\\. Quit fully with CMD+Q, restart Claude, and paste the prompt into a new chat.",
8
8
  "author": {
@@ -501,19 +501,29 @@ class S4LMenuBar(rumps.App):
501
501
  except Exception:
502
502
  continue
503
503
  low = txt.lower()
504
- # (a) Human "usage limit reached" prose (the older shape).
505
- if "weekly limit" in low or "usage limit" in low or "hit your limit" in low:
504
+ # CRITICAL: only treat this as a limit when it is an actual API
505
+ # ERROR in this run NEVER loose prose anywhere in the transcript.
506
+ # The drafting prompt embeds candidate threads + the feedback report,
507
+ # which frequently contain phrases like "weekly limit" / "rate limit"
508
+ # as CONTENT (an AI-product timeline is full of them — a 'claude-meter'
509
+ # example post "reached your weekly limit by tuesday" false-tripped the
510
+ # old prose match on 2026-06-29). The api-error markers below are set
511
+ # by the SDK only on real errors, so they can't appear in content.
512
+ #
513
+ # (a) HTTP-429 rate_limit — the definitive routines-lane shape:
514
+ # {"error":"rate_limit",...,"isApiErrorMessage":true,"apiErrorStatus":429}
515
+ if '"apierrorstatus":429' in low or '"error":"rate_limit"' in low:
516
+ limit_msg = "Claude rate limit reached (429)"
517
+ break
518
+ # (b) Weekly/usage-limit prose, but ONLY when carried inside a real
519
+ # API-error message (gated on the marker so content can't trip it).
520
+ if '"isapierrormessage":true' in low and (
521
+ "weekly limit" in low or "usage limit" in low or "hit your limit" in low
522
+ ):
506
523
  import re
507
524
  m = re.search(r"resets [^\"\\]{0,40}", txt)
508
525
  limit_msg = m.group(0).strip().rstrip(".") if m else "Claude usage limit reached"
509
526
  break
510
- # (b) HTTP-429 rate_limit api_error — the shape the Claude Desktop
511
- # routines lane ACTUALLY returns when the account is over its limit.
512
- # The prose strings above never appear in this case; the routine
513
- # transcript carries {"error":"rate_limit",...,"apiErrorStatus":429}.
514
- if '"apierrorstatus":429' in low or '"error":"rate_limit"' in low:
515
- limit_msg = "Claude rate limit reached (429)"
516
- break
517
527
  except Exception:
518
528
  pass
519
529
  return ran, limit_msg
@@ -50,7 +50,7 @@ MILESTONE_LABELS = {
50
50
  "mode_chosen": "Mode chosen",
51
51
  "project_ready": "Project ready",
52
52
  "topics_seeded": "Topics seeded",
53
- "tasks_scheduled": "Autopilot scheduled",
53
+ "tasks_scheduled": "Tasks scheduled",
54
54
  }
55
55
 
56
56
  # Mirrors index.ts TWITTER_AUTOPILOT_LABEL.
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/social-autoposter-mcp",
3
- "version": "1.6.147",
3
+ "version": "1.6.149",
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": "social-autoposter",
3
- "version": "1.6.147",
3
+ "version": "1.6.149",
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"
@@ -1772,14 +1772,18 @@ log "Engagement style assigned: mode=$PICKED_MODE style=${PICKED_STYLE:-(invent)
1772
1772
  # The arm is stamped onto every post this cycle via SAPS_DRAFT_PROMPT_VARIANT
1773
1773
  # (read by twitter_post_plan.py -> log_post.py -> posts.draft_prompt_variant),
1774
1774
  # mirroring the tail_link_variant plumbing. Split tunable via
1775
- # TWITTER_DRAFT_PROMPT_AB_RATE (fraction assigned to 'treatment', default 0.5).
1776
- DRAFT_PROMPT_AB_RATE="${TWITTER_DRAFT_PROMPT_AB_RATE:-0.5}"
1775
+ # TWITTER_DRAFT_PROMPT_AB_RATE (fraction assigned to 'treatment'). DEFAULT 0 =
1776
+ # 100% control = experiment OFF (no behavior change anywhere until explicitly
1777
+ # enabled, mirroring how the "written with ai" tag is off by default). Set it to
1778
+ # e.g. 0.5 in .env to activate a 50/50 split. The dashboard reads the SAME var
1779
+ # with the SAME default (bin/server.js), so display and routing never diverge.
1780
+ DRAFT_PROMPT_AB_RATE="${TWITTER_DRAFT_PROMPT_AB_RATE:-0}"
1777
1781
  SAPS_DRAFT_PROMPT_VARIANT=$(python3 -c "
1778
1782
  import random
1779
1783
  try:
1780
1784
  rate = float('$DRAFT_PROMPT_AB_RATE')
1781
1785
  except Exception:
1782
- rate = 0.5
1786
+ rate = 0.0
1783
1787
  rate = min(1.0, max(0.0, rate))
1784
1788
  print('treatment' if random.random() < rate else 'control')
1785
1789
  " 2>/dev/null || echo control)