@m13v/s4l 1.7.1-rc.8 → 1.7.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.
Files changed (47) hide show
  1. package/bin/cli.js +23 -0
  2. package/mcp/dist/index.js +354 -24
  3. package/mcp/dist/panel.html +42 -31
  4. package/mcp/dist/product-link.html +1 -1
  5. package/mcp/dist/runtime.js +23 -0
  6. package/mcp/dist/telemetry.js +80 -3
  7. package/mcp/dist/version.json +2 -2
  8. package/mcp/install.mjs +0 -15
  9. package/mcp/manifest.json +9 -1
  10. package/mcp/menubar/dashboard_server.py +12 -0
  11. package/mcp/menubar/s4l_card.py +539 -80
  12. package/mcp/menubar/s4l_menubar.py +305 -199
  13. package/mcp/menubar/s4l_state.py +228 -33
  14. package/mcp/package.json +1 -1
  15. package/mcp/shared/doctor.cjs +8 -0
  16. package/package.json +4 -1
  17. package/scripts/active_users.py +3 -6
  18. package/scripts/autopilot_stall_watch.py +200 -16
  19. package/scripts/claude_job.py +6 -0
  20. package/scripts/feedback_digest.py +34 -6
  21. package/scripts/get-latest-staging-mcpb.sh +28 -45
  22. package/scripts/harness_overlay.py +45 -0
  23. package/scripts/identity.py +26 -0
  24. package/scripts/learned_preferences.py +241 -32
  25. package/scripts/link_tail.py +167 -91
  26. package/scripts/mark_event.py +128 -0
  27. package/scripts/memory_snapshot.py +27 -0
  28. package/scripts/merge_review_queue.py +42 -16
  29. package/scripts/pick_twitter_thread_target.py +1 -1
  30. package/scripts/release-mcpb.sh +48 -0
  31. package/scripts/s4l_mode.py +7 -5
  32. package/scripts/scan_pii.py +9 -1
  33. package/scripts/schedule_state.py +146 -69
  34. package/scripts/scheduled_task_selfheal.py +276 -0
  35. package/scripts/scheduled_tasks_snapshot.py +107 -0
  36. package/scripts/send_gmail_report.py +76 -0
  37. package/scripts/sentry_digest.py +81 -189
  38. package/scripts/sentry_init.py +43 -15
  39. package/scripts/snapshot.py +20 -0
  40. package/scripts/twitter_post_plan.py +39 -13
  41. package/skill/dm-outreach-twitter.sh +19 -0
  42. package/skill/engage-twitter.sh +27 -0
  43. package/skill/run-draft-and-publish.sh +13 -12
  44. package/skill/run-linkedin.sh +9 -1
  45. package/skill/run-twitter-cycle.sh +199 -27
  46. package/skill/sentry-digest.sh +125 -12
  47. package/scripts/set-channel.sh +0 -29
@@ -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}.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}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">
@@ -676,6 +676,29 @@ async function provision(progress) {
676
676
  /* best effort; S4L_REPO_DIR + the run-*.sh fallback also resolve the repo */
677
677
  }
678
678
  setStep("repo", "done", `unpacked to ${resolvedRepo}`);
679
+ // Auto-opt a bare .mcpb FIRST install into the staging channel when the
680
+ // shipped build is itself a pre-release (-rc.N) — e.g. downloaded from
681
+ // the staging-latest alias link (scripts/release-mcpb.sh step 7b).
682
+ // Without this, channel.json stays absent, which releaseChannel()'s
683
+ // fail-safe default reads as "stable" (mcp/src/version.ts), so the box
684
+ // would install this one rc and then silently stop tracking staging
685
+ // (never pick up the next rc). Mirrors the same one-time write in
686
+ // bin/cli.js's installMcp() for the npx install path. Gated to this
687
+ // `else` branch (genuine bare-.mcpb unpack, not envClone) so a
688
+ // developer's own npm/git clone is never auto-enrolled. Only writes when
689
+ // no channel marker exists yet — never overrides an existing preference.
690
+ try {
691
+ const bv = bundledVersion();
692
+ const channelPath = path.join(STATE_DIR, "channel.json");
693
+ if (bv && bv.includes("-rc.") && !fs.existsSync(channelPath)) {
694
+ fs.mkdirSync(STATE_DIR, { recursive: true });
695
+ fs.writeFileSync(channelPath, JSON.stringify({ channel: "staging" }, null, 2) + "\n", "utf-8");
696
+ console.error(`[runtime] first install of a staging build (${bv}) — opted into the staging channel`);
697
+ }
698
+ }
699
+ catch {
700
+ /* best effort; worst case the box just tracks stable */
701
+ }
679
702
  }
680
703
  // --- Step 1: uv -----------------------------------------------------------
681
704
  setStep("uv", "running");
@@ -18,6 +18,9 @@ import { VERSION } from "./version.js";
18
18
  const EMBEDDED_DSN = "https://4d44ac907262c6545cf8681703528d04@o4507617161314304.ingest.us.sentry.io/4511598804336640";
19
19
  const SENTRY_DSN = process.env.S4L_SENTRY_DSN || EMBEDDED_DSN;
20
20
  let sentryReady = false;
21
+ // Cached install_id (set by tagInstall), reused to fingerprint every event per
22
+ // install — see captureError/captureMessage below.
23
+ let cachedInstallId = null;
21
24
  export function initSentry() {
22
25
  if (sentryReady || !SENTRY_DSN)
23
26
  return;
@@ -51,8 +54,10 @@ async function tagInstall() {
51
54
  if (res.code !== 0)
52
55
  return;
53
56
  const id = JSON.parse(res.stdout || "{}");
54
- if (id.install_id)
57
+ if (id.install_id) {
55
58
  Sentry.setTag("install_id", String(id.install_id));
59
+ cachedInstallId = String(id.install_id);
60
+ }
56
61
  if (id.hostname)
57
62
  Sentry.setTag("hostname", String(id.hostname));
58
63
  }
@@ -62,8 +67,41 @@ async function tagInstall() {
62
67
  }
63
68
  export function captureError(err, tags) {
64
69
  try {
65
- if (sentryReady)
66
- Sentry.captureException(err, tags ? { tags } : undefined);
70
+ if (!sentryReady)
71
+ return;
72
+ Sentry.withScope((scope) => {
73
+ for (const [k, v] of Object.entries(tags || {}))
74
+ scope.setTag(k, v);
75
+ // Mix install_id into the grouping key so the SAME error on two different
76
+ // customer boxes lands in two different issues, not one conflated issue
77
+ // whose "latest event" can silently belong to a different install than
78
+ // the one you filtered for (default grouping ignores tags entirely).
79
+ if (cachedInstallId)
80
+ scope.setFingerprint(["{{ default }}", cachedInstallId]);
81
+ Sentry.captureException(err);
82
+ });
83
+ }
84
+ catch {
85
+ /* swallow */
86
+ }
87
+ }
88
+ // Report a handled, non-exception CONDITION (mirrors scripts/sentry_init.py's
89
+ // capture_message on the Python side — added 2026-07-07 alongside it so both
90
+ // halves of the client have parity). Use for operational states, not thrown
91
+ // errors: captureError only covers exceptions.
92
+ export function captureMessage(message, opts) {
93
+ try {
94
+ if (!sentryReady)
95
+ return;
96
+ Sentry.withScope((scope) => {
97
+ for (const [k, v] of Object.entries(opts?.tags || {}))
98
+ scope.setTag(k, v);
99
+ for (const [k, v] of Object.entries(opts?.extra || {}))
100
+ scope.setExtra(k, v);
101
+ if (cachedInstallId)
102
+ scope.setFingerprint(["{{ default }}", cachedInstallId]);
103
+ Sentry.captureMessage(message, opts?.level || "info");
104
+ });
67
105
  }
68
106
  catch {
69
107
  /* swallow */
@@ -78,6 +116,45 @@ export async function flushSentry(ms = 2000) {
78
116
  /* swallow */
79
117
  }
80
118
  }
119
+ function lastVersionPath() {
120
+ return path.join(snapshotStateDir(), "last-version.json");
121
+ }
122
+ // Detect a version change on boot (a .mcpb self-update, or Desktop reloading
123
+ // the extension after one) and log ONE explicit milestone: old_version ->
124
+ // new_version. Before this, "was this an update?" had to be inferred after
125
+ // the fact by cross-referencing THREE unrelated weak signals — app_version in
126
+ // memory_snapshot, reason="startup" in installation_state_snapshots, and the
127
+ // menu bar's own boot line — across a manual full-day log sweep (the Karol
128
+ // 2026-07-07 update-orphan investigation). Call once, early in main(), before
129
+ // anything else that might restart or exit. Best-effort; never throws.
130
+ export function checkVersionChange() {
131
+ const to = VERSION;
132
+ let from = null;
133
+ try {
134
+ const raw = fs.readFileSync(lastVersionPath(), "utf-8");
135
+ from = JSON.parse(raw)?.version ?? null;
136
+ }
137
+ catch {
138
+ from = null; // no cached version -> first boot ever, or file missing/corrupt
139
+ }
140
+ const isFirstBoot = from === null;
141
+ const changed = !isFirstBoot && from !== to;
142
+ try {
143
+ fs.mkdirSync(snapshotStateDir(), { recursive: true });
144
+ fs.writeFileSync(lastVersionPath(), JSON.stringify({ version: to, seen_at: new Date().toISOString() }), "utf-8");
145
+ }
146
+ catch {
147
+ /* best-effort; a failed write just means the next boot re-detects from "from: null" */
148
+ }
149
+ if (changed) {
150
+ console.error(`[social-autoposter-mcp] self-update detected: ${from} -> ${to}`);
151
+ captureMessage(`social-autoposter self-update: ${from} -> ${to}`, {
152
+ level: "info",
153
+ tags: { component: "update", issue: "self_update", from_version: from || "", to_version: to },
154
+ });
155
+ }
156
+ return { changed, isFirstBoot, from, to };
157
+ }
81
158
  // Phone home so .mcpb installs show up in the install-lane digest, parity with
82
159
  // the npx launchd heartbeat. Best-effort; never throws.
83
160
  export async function sendHeartbeat(reason) {
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.7.1-rc.8",
3
- "installedAt": "2026-07-07T23:28:11.193Z"
2
+ "version": "1.7.1",
3
+ "installedAt": "2026-07-09T00:28:03.233Z"
4
4
  }
package/mcp/install.mjs CHANGED
@@ -14,8 +14,6 @@ import path from "node:path";
14
14
 
15
15
  const SERVER_KEY = "social-autoposter";
16
16
  const UNINSTALL = process.argv.includes("--uninstall");
17
- const CHANNEL_STAGING = process.argv.includes("--channel-staging");
18
- const CHANNEL_STABLE = process.argv.includes("--channel-stable");
19
17
 
20
18
  // ---- resolve the absolute paths we want pinned into the spawn env ----------
21
19
  const here = path.dirname(new URL(import.meta.url).pathname);
@@ -137,19 +135,6 @@ for (const t of targets) {
137
135
  }
138
136
  }
139
137
 
140
- // Set release channel if requested
141
- if (!UNINSTALL && (CHANNEL_STAGING || CHANNEL_STABLE)) {
142
- const stateDir = path.join(home, ".social-autoposter-mcp");
143
- const channel = CHANNEL_STAGING ? "staging" : "stable";
144
- try {
145
- fs.mkdirSync(stateDir, { recursive: true });
146
- writeJson(path.join(stateDir, "channel.json"), { channel });
147
- console.log(`✓ Release channel set to: ${channel}`);
148
- } catch (err) {
149
- console.error(`✗ Failed to set channel: ${err.message}`);
150
- }
151
- }
152
-
153
138
  console.log("");
154
139
  if (ok === targets.length) {
155
140
  if (UNINSTALL) {
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.1-rc.8",
5
+ "version": "1.7.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,10 +48,18 @@
48
48
  "name": "restart_menubar",
49
49
  "description": "Restart the S4L menu bar app"
50
50
  },
51
+ {
52
+ "name": "pause_s4l",
53
+ "description": "Pause or resume S4L"
54
+ },
51
55
  {
52
56
  "name": "report_diagnosis",
53
57
  "description": "Send a diagnosis report to the S4L developers"
54
58
  },
59
+ {
60
+ "name": "client_event",
61
+ "description": "Log a lightweight client UI event"
62
+ },
55
63
  {
56
64
  "name": "queue_setup",
57
65
  "description": "Get autopilot scheduled-task specs"
@@ -23,6 +23,8 @@ import sys
23
23
  import threading
24
24
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
25
25
 
26
+ import s4l_state as st # mcp/menubar/s4l_state.py — shared pause_s4l/resume_s4l
27
+
26
28
  HOME = os.path.expanduser("~")
27
29
 
28
30
 
@@ -200,7 +202,17 @@ def _handle_tool(name, args):
200
202
  "runtime_ready": s.get("runtime_ready"),
201
203
  "provisioning": s.get("runtime_provisioning"),
202
204
  "onboarding": s.get("onboarding"),
205
+ "paused": st.is_paused(),
203
206
  })
207
+ if name == "pause_s4l":
208
+ action = args.get("action") or "status"
209
+ if action == "pause":
210
+ res = st.pause_s4l()
211
+ elif action == "resume":
212
+ res = st.resume_s4l()
213
+ else:
214
+ res = {"ok": True, "paused": st.is_paused()}
215
+ return _result({"action": action, **res})
204
216
  if name == "engagement_mode":
205
217
  action = args.get("action") or "get"
206
218
  if action == "toggle":