@m13v/s4l 1.7.5-rc.2 → 1.7.5-rc.21
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 +571 -79
- package/mcp/dist/panel.html +2 -3
- package/mcp/dist/redditAuth.js +115 -0
- package/mcp/dist/runtime.js +83 -2
- package/mcp/dist/setup.js +142 -5
- package/mcp/dist/telemetry.js +13 -3
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_card.py +32 -6
- package/mcp/menubar/s4l_menubar.py +42 -111
- package/mcp/menubar/s4l_state.py +54 -187
- package/mcp/package.json +1 -1
- package/mcp/shared/onboarding-ledger.cjs +63 -4
- package/package.json +1 -1
- package/scripts/autopilot_stall_watch.py +3 -6
- package/scripts/browser_mutex.py +316 -0
- package/scripts/claude_job.py +21 -0
- package/scripts/config.py +118 -0
- package/scripts/counterparty_history.py +44 -6
- package/scripts/engage_github.py +7 -3
- package/scripts/engage_reddit.py +9 -3
- package/scripts/engagement_signal_feedback.py +256 -0
- package/scripts/learned_preferences.py +37 -14
- package/scripts/memory_snapshot.py +45 -0
- package/scripts/merge_review_queue.py +67 -3
- package/scripts/post_reddit.py +289 -24
- package/scripts/reddit_browser.py +62 -63
- package/scripts/reddit_browser_fetch.py +32 -0
- package/scripts/release-mcpb.sh +17 -0
- package/scripts/reset-test-machine.sh +13 -7
- package/scripts/scan_twitter_thread_followups.py +7 -1
- package/scripts/schedule_state.py +1 -1
- package/scripts/setup_reddit_auth.py +925 -0
- package/scripts/snapshot.py +96 -4
- package/scripts/stats.py +37 -13
- package/scripts/test_no_silent_fallbacks.py +281 -0
- package/scripts/trigger_promo_lane_test.sh +91 -0
- package/scripts/twitter_browser.py +183 -342
- package/scripts/twitter_scan.py +75 -10
- package/scripts/watchdog_hung_runs.py +15 -9
- package/skill/lib/harness-common.sh +344 -0
- package/skill/lib/linkedin-backend.sh +61 -161
- package/skill/lib/reddit-backend.sh +39 -125
- package/skill/lib/twitter-backend.sh +51 -273
- package/skill/lock.sh +57 -0
- package/skill/run-reddit-search-launchd.sh +4 -1
- package/skill/run-reddit-search.sh +51 -2
- package/skill/run-twitter-cycle.sh +44 -21
package/mcp/dist/panel.html
CHANGED
|
@@ -70,7 +70,7 @@ Boolean requesting whether a visible border and background is provided by the ho
|
|
|
70
70
|
- omitted: host decides border`)});m({method:u("ui/request-display-mode"),params:m({mode:st.describe("The display mode being requested.")})});var Eh=m({mode:st.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),Dh=U([u("model"),u("app")]).describe("Tool visibility scope - who can access the tool.");m({resourceUri:d().optional(),visibility:x(Dh).optional().describe(`Who can access this tool. Default: ["model", "app"]
|
|
71
71
|
- "model": Tool visible to and callable by the agent
|
|
72
72
|
- "app": Tool callable by the app from this server only`),csp:Te().optional(),permissions:Te().optional()});m({mimeTypes:x(d()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')});m({method:u("ui/download-file"),params:m({contents:x(U([Ol,Tl])).describe("Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types.")})});m({method:u("ui/message"),params:m({role:u("user").describe('Message role, currently only "user" is supported.'),content:x(jt).describe("Message content blocks (text, image, etc.).")})});m({method:u("ui/notifications/sandbox-resource-ready"),params:m({html:d().describe("HTML content to load into the inner iframe."),sandbox:d().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:xo.optional().describe("CSP configuration from resource metadata."),permissions:jo.optional().describe("Sandbox permissions from resource metadata.")})});var Rh=m({method:u("ui/notifications/tool-result"),params:Qn.describe("Standard MCP tool execution result.")}),Cl=m({toolInfo:m({id:kt.optional().describe("JSON-RPC id of the tools/call request."),tool:zo.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:yh.optional().describe("Current color theme preference."),styles:Oh.optional().describe("Style configuration for theming the app."),displayMode:st.optional().describe("How the UI is currently displayed."),availableDisplayModes:x(st).optional().describe("Display modes the host supports."),containerDimensions:U([m({height:O().describe("Fixed container height in pixels.")}),m({maxHeight:U([O(),it()]).optional().describe("Maximum container height in pixels.")})]).and(U([m({width:O().describe("Fixed container width in pixels.")}),m({maxWidth:U([O(),it()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
|
|
73
|
-
container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:d().optional().describe("User's language and region preference in BCP 47 format."),timeZone:d().optional().describe("User's timezone in IANA format."),userAgent:d().optional().describe("Host application identifier."),platform:U([u("web"),u("desktop"),u("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:m({touch:H().optional().describe("Whether the device supports touch input."),hover:H().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(),Ch=m({method:u("ui/notifications/host-context-changed"),params:Cl.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:x(jt).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:A(d(),J().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});m({method:u("ui/initialize"),params:m({appInfo:Kn.describe("App identification (name and version)."),appCapabilities:Uh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var Zh=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Kn.describe("Host application identification and version."),hostCapabilities:Ph.describe("Features and capabilities provided by the host."),hostContext:Cl.describe("Rich context about the host environment.")}).passthrough(),Ah={target:"draft-2020-12"};async function ta(e,n){let r=e["~standard"];if(r.jsonSchema)return r.jsonSchema[n](Ah);if(r.vendor==="zod"){let{z:o}=await td(()=>Promise.resolve().then(()=>zp),void 0,import.meta.url);return o.toJSONSchema(e,{io:n})}throw Error(`Schema (vendor: ${r.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function na(e,n,r=""){let o=await e["~standard"].validate(n);if(o.issues){let t=o.issues.map(i=>{var s;let a=(s=i.path)==null?void 0:s.map(c=>typeof c=="object"?c.key:c).join(".");return a?`${a}: ${i.message}`:i.message}).join("; ");throw Error(r+t)}return o.value}function Lh(e){let n=document.documentElement;n.setAttribute("data-theme",e),n.style.colorScheme=e}function Mh(e,n=document.documentElement){for(let[r,o]of Object.entries(e))o!==void 0&&n.style.setProperty(r,o)}function qh(e){if(document.getElementById("__mcp-host-fonts"))return;let n=document.createElement("style");n.id="__mcp-host-fonts",n.textContent=e,document.head.appendChild(n)}const Bt=class Bt extends gh{constructor(r,o={},t={autoResize:!0}){super(t);C(this,"_appInfo");C(this,"_capabilities");C(this,"options");C(this,"_hostCapabilities");C(this,"_hostInfo");C(this,"_hostContext");C(this,"_registeredTools",{});C(this,"_initializedSent",!1);C(this,"eventSchemas",{toolinput:zh,toolinputpartial:xh,toolresult:Rh,toolcancelled:jh,hostcontextchanged:Ch});C(this,"_everHadListener",new Set);C(this,"_toolHandlersInitialized",!1);C(this,"_onteardown");C(this,"_oncalltool");C(this,"_onlisttools");C(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=t,t.allowUnsafeEval||ee({jitless:!0}),this.setRequestHandler(Gn,i=>(console.log("Received ping:",i.params),{})),this.setEventHandler("hostcontextchanged",void 0)}_assertInitialized(r){var t;if(this._initializedSent)return;let o=`[ext-apps] App.${r}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if((t=this.options)!=null&&t.strict)throw Error(o);console.warn(`${o}. This will throw in a future release.`)}_assertHandlerTiming(r){var t;if(!Bt.ONE_SHOT_EVENTS.has(r)||this._everHadListener.has(r)||(this._everHadListener.add(r),!this._initializedSent))return;let o=`[ext-apps] "${String(r)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if((t=this.options)!=null&&t.strict)throw Error(o);console.warn(o)}setEventHandler(r,o){o&&this._assertHandlerTiming(r),super.setEventHandler(r,o)}addEventListener(r,o){this._assertHandlerTiming(r),super.addEventListener(r,o)}onEventDispatch(r,o){r==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...o})}registerCapabilities(r){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._capabilities=hh(this._capabilities,r)}registerTool(r,o,t){if(this._registeredTools[r])throw Error(`Tool ${r} is already registered`);let i=this,a=()=>{var p;i._initializedSent&&((p=i._capabilities.tools)!=null&&p.listChanged)&&i.sendToolListChanged()},s=o.inputSchema!==void 0,c={title:o.title,description:o.description,inputSchema:o.inputSchema,outputSchema:o.outputSchema,annotations:o.annotations,_meta:o._meta,enabled:!0,enable(){this.enabled=!0,a()},disable(){this.enabled=!1,a()},update(p){Object.assign(this,p),a()},remove(){i._registeredTools[r]===c&&(delete i._registeredTools[r],a())},handler:async(p,h)=>{if(!c.enabled)throw Error(`Tool ${r} is disabled`);let f;if(s){let _=c.inputSchema,w=_?await na(_,p??{},`Invalid input for tool ${r}: `):p??{};f=await t(w,h)}else f=await t(h);return c.outputSchema&&!f.isError&&(f.structuredContent=await na(c.outputSchema,f.structuredContent,`Invalid output for tool ${r}: `)),f}};return this._registeredTools[r]=c,!this._capabilities.tools&&!this.transport&&this.registerCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),a(),c}ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(r,o)=>{let t=this._registeredTools[r.name];if(!t)throw Error(`Tool ${r.name} not found`);return t.handler(r.arguments,o)},this.onlisttools=async(r,o)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([t,i])=>i.enabled).map(async([t,i])=>{let a={name:t,title:i.title,description:i.description,inputSchema:i.inputSchema?await ta(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await ta(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(Th,(o,t)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(o.params,t)})}get oncalltool(){return this._oncalltool}set oncalltool(r){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,r),this._oncalltool=r,this.replaceRequestHandler(Ul,(o,t)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(o.params,t)})}get onlisttools(){return this._onlisttools}set onlisttools(r){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,r),this._onlisttools=r,this.replaceRequestHandler(Pl,(o,t)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(o.params,t)})}assertCapabilityForMethod(r){var o;switch(r){case"sampling/createMessage":if(!((o=this._hostCapabilities)!=null&&o.sampling))throw Error(`Host does not support sampling (required for ${r})`);break}}assertRequestHandlerCapability(r){switch(r){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${r})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${r} registered`)}}assertNotificationCapability(r){}assertTaskCapability(r){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(r){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(r,o){if(this._assertInitialized("callServerTool"),typeof r=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${r}"). Did you mean: callServerTool({ name: "${r}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:r},Qn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},Nl,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},jl,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let t=r.tools?Rl:Dl;return await this.request({method:"sampling/createMessage",params:r},t,o)}sendMessage(r,o){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:r},Ih,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},lo,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},Sh,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},wh,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},Eh,o)}sendSizeChanged(r){return this.notification({method:"ui/notifications/size-changed",params:r})}setupSizeChangedNotifications(){let r=!1,o=0,t=0,i=()=>{r||(r=!0,requestAnimationFrame(()=>{r=!1;let s=document.documentElement,c=s.style.height;s.style.height="max-content";let p=Math.ceil(s.getBoundingClientRect().height);s.style.height=c;let h=Math.ceil(window.innerWidth);(h!==o||p!==t)&&(o=h,t=p,this.sendSizeChanged({width:h,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(r=new bh(window.parent,window.parent),o){var t;if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1,await super.connect(r);try{let i=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:vh}},Zh,o);if(i===void 0)throw Error(`Server sent invalid initialize result: ${i}`);this._hostCapabilities=i.hostCapabilities,this._hostInfo=i.hostInfo,this._hostContext=i.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,(t=this.options)!=null&&t.autoResize&&this.setupSizeChangedNotifications()}catch(i){throw this.close(),i}}};C(Bt,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let dr=Bt;function ra(e){const n=e&&e.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(e&&e.content||[]).find(o=>o&&o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{}}return{}}class Hh{constructor(){C(this,"onhostcontextchanged");C(this,"onerror");C(this,"ontoolresult")}async connect(){var n,r;try{const[o,t]=await Promise.all([this.callServerTool({name:"project_config",arguments:{status:!0}}),this.callServerTool({name:"runtime",arguments:{action:"status"}})]),i=ra(o),a=ra(t),s=Array.isArray(i.projects)?i.projects:[],c={projects:s,projects_total:s.length,projects_ready:s.filter(p=>p&&p.ready).length,x_connected:!!i.x_connected,x_state:i.x_state||"",x_handle:i.x_handle??null,version:i.mcp_version||"",latest_version:i.latest_version??null,update_available:!!i.update_available,runtime_ready:typeof a.runtime_ready=="boolean"?a.runtime_ready:!0,runtime_provisioning:!!a.provisioning,onboarding:a.onboarding||i.onboarding};(n=this.ontoolresult)==null||n.call(this,{structuredContent:{snapshot:JSON.stringify(c)}})}catch(o){(r=this.onerror)==null||r.call(this,o)}}getHostContext(){}async callServerTool(n){const r=await fetch(`/tool/${encodeURIComponent(n.name)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n.arguments??{})});if(!r.ok){let o=`HTTP ${r.status}`;try{o=await r.text()||o}catch{}return{isError:!0,content:[{type:"text",text:o}]}}return r.json()}async sendMessage(){return{isError:!0}}}function Fh(){return globalThis.__S4L_BRIDGE__==="http"?new Hh:new dr({name:"S4L Panel",version:"1.0.0"})}function Zl(e){const n=e.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(e.content||[]).find(o=>o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{_raw:r.text}}return{}}const D=e=>document.getElementById(e),Al=D("ver"),Xe=D("btn-setup"),Ht=D("btn-schedule"),Pt=D("stats-grid"),er=D("stats-toggle"),Jh=D("log"),Bh=D("install-card"),Ne=D("setup-summary"),Ll=D("onboarding-details"),Vh=D("onboarding-steps"),tr=D("onboarding-blocker"),ia=D("onboarding-count"),Wh=D("onboarding-bar-fill"),Kh=D("live-card"),Gh=D("stats-card"),oa=D("install-steps"),Ae=D("install-err"),_e=D("btn-install"),Qh=D("menubar-banner"),aa=D("btn-menubar-restart"),No=D("btn-live"),Oo=D("btn-live-stop"),sa=D("btn-live-front"),Le=D("live-status"),Ft=D("live-img"),Ml=D("switch-personal"),ql=D("switch-promo"),ve=D("switch-pause"),Yh=D("pause-name"),Xh=D("split-row"),eg=D("split-desc"),$e=D("split-slider"),Ce=D("posting-volume-select"),tg=D("posting-volume-desc"),ng=D("mode-sub"),rg=D("settings-card"),nr=D("settings-toggle"),fe=D("settings-body");let E=null,rr=!1,Ut=!1,ir=!1,ke=!1,Yn=!1,Qe=!1,De=!1,or=!1;function F(e){Jh.textContent=e}function ig(e){switch(e){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function To(e){if(!e||!Array.isArray(e.steps)){oa.innerHTML="";return}oa.innerHTML=e.steps.map(n=>{const r=n.detail&&n.status!=="pending"?` <span class="detail">${n.status==="error"?n.detail:""}</span>`:"";return`<li class="${n.status}"><span class="glyph">${ig(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),e.error?(Ae.textContent=e.error,Ae.hidden=!1):Ae.hidden=!0}const og={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 ag(e){switch(e){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function mr(){Ll.hidden=!ke,Ne.setAttribute("aria-expanded",String(ke)),Ne.classList.toggle("expanded",ke)}function Hl(e){return e?e.setup_complete!==void 0?!!e.setup_complete:!!e.runtime_ready&&(e.projects_ready||0)>0&&!!e.x_connected:!1}function sg(e){if(!e||!Array.isArray(e.milestones)){Ne.hidden=!0,Ll.hidden=!0;return}const n=Hl(E);Ne.hidden=n,n&&ke&&(ke=!1,mr());const r=e.milestones.length,o=e.milestones.filter(i=>i.status==="complete").length,t=!!e.current_blocker&&!n;Ne.classList.toggle("complete",n),Ne.classList.toggle("blocked",t),ia.hidden=n,ia.textContent=t?`${o}/${r} · needs you`:Ut?`${o}/${r} · setting up…`:`${o}/${r}`,Wh.style.width=r>0?`${Math.round(o/r*100)}%`:"0%",Vh.innerHTML=e.milestones.map(i=>{const a=og[i.id]||i.id,s=i.attempts>1?` <span class="detail">${i.attempts} attempts</span>`:"";return`<li class="${i.status}"><span class="glyph">${ag(i.status)}</span><span>${a}${s}</span></li>`}).join(""),e.current_blocker?(tr.textContent=`Current blocker: ${e.current_blocker.message}`,tr.hidden=!1,ke=!0):tr.hidden=!0,mr()}function Jt(){if(!E)return;sg(E.onboarding),Al.innerHTML=E.update_available&&E.latest_version?`v${E.version} · <button id="btn-update" class="update-btn">Update to ${E.latest_version}</button>`:`v${E.version}`;const e=!E.runtime_ready;Bh.hidden=!e,Qh.hidden=e||E.menubar_running!==!1;const n=Hl(E);Xe.hidden=n,Xe.disabled=!1,Xe.classList.toggle("primary",!n);const r=n&&(E.schedule_state==="missing"||E.schedule_state==="disabled"||E.schedule_state==="stalled");Ht.hidden=!r,Ht.classList.toggle("primary",r);const o=E.flags||(E.mode==="promotion"?{personal_brand:!1,promotion:!0}:{personal_brand:!0,promotion:!1}),t=!!o.personal_brand,i=!!o.promotion;Ml.setAttribute("aria-checked",String(t)),ql.setAttribute("aria-checked",String(i));const a=Math.round((typeof E.personal_brand_share=="number"?E.personal_brand_share:.5)*100);Xh.hidden=!(t&&i),Yn||($e.value=String(a),Vl(a)),ng.textContent=t&&i?`Both lanes on: cycles split ${a}/${100-a} personal/promotion.`:!t&&!i?"No lane on; the cycle falls back to personal brand.":"";const s=E.paused===!0;ve.setAttribute("aria-checked",String(!s)),ve.setAttribute("aria-label",s?"Resume S4L":"Pause S4L"),ve.title=s?"Resume S4L":"Pause S4L",Yh.textContent=s?"Paused":"Running",Kh.hidden=!n,Gh.hidden=!n,rg.hidden=e}function be(e){E={...E||{},...e},Jt()}function cg(e){const n=Array.isArray(e.projects)?e.projects:[];return{projects:n,projects_total:n.length,projects_ready:n.filter(r=>r.ready).length,x_connected:!!e.x_connected,x_state:e.x_state||"",x_handle:e.x_handle??null,...e.setup_complete!==void 0?{setup_complete:!!e.setup_complete}:{},version:e.mcp_version||(E==null?void 0:E.version)||"",latest_version:e.latest_version??null,update_available:!!e.update_available,mode:e.mode??(E==null?void 0:E.mode),flags:e.flags??(E==null?void 0:E.flags),personal_brand_share:e.personal_brand_share??(E==null?void 0:E.personal_brand_share),onboarding:e.onboarding}}const Ie=Fh();function Fl(e){var n,r,o;e.theme&&Lh(e.theme),(n=e.styles)!=null&&n.variables&&Mh(e.styles.variables),(o=(r=e.styles)==null?void 0:r.css)!=null&&o.fonts&&qh(e.styles.css.fonts)}Ie.onhostcontextchanged=Fl;Ie.onerror=e=>console.error(e);Ie.ontoolresult=e=>{const n=Zl(e);n&&typeof n.projects_total=="number"&&(be(n),n.runtime_ready?Jl():n.runtime_provisioning&&Po())};async function Y(e,n={}){const r=await Ie.callServerTool({name:e,arguments:n});return Zl(r)}async function ge(){F("Refreshing…");try{const[e,n]=await Promise.all([Y("project_config",{status:!0}),Y("runtime",{action:"status"}).catch(()=>({}))]);be({...cg(e),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},...typeof n.menubar_running=="boolean"?{menubar_running:n.menubar_running}:{},...typeof n.paused=="boolean"?{paused:n.paused}:{},onboarding:n.onboarding||e.onboarding||(E==null?void 0:E.onboarding)}),E&&!E.runtime_ready&&n.provisioning&&Po(),F(""),Jl()}catch(e){F("Refresh failed: "+((e==null?void 0:e.message)||e))}}async function Po(){if(!rr){rr=!0,_e.disabled=!0,_e.textContent="Installing…";try{for(;;){const e=await Y("runtime",{action:"status"}).catch(()=>({}));if(To(e.progress??null),e.onboarding&&be({onboarding:e.onboarding}),e.runtime_ready){be({runtime_ready:!0}),F("Runtime installed; you're ready to set up."),ge();return}const n=e.progress??null;if(n&&n.done&&!n.ok){_e.disabled=!1,_e.textContent="Retry install",F("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{rr=!1}}}async function ug(){var r;if(Ut)return;Ut=!0,Jt();const e=Date.now(),n=1200*1e3;try{for(;;){const o=await Y("runtime",{action:"status"}).catch(()=>({}));o.progress&&To(o.progress);const t={};if(typeof o.runtime_ready=="boolean"&&(t.runtime_ready=o.runtime_ready),o.onboarding&&(t.onboarding=o.onboarding),Object.keys(t).length&&be(t),(r=o.onboarding)!=null&&r.complete){await ge(),F("Setup complete.");break}if(Date.now()-e>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{Ut=!1,Jt()}}async function Jl(){try{const e=await Y("get_stats",{days:7}),n=Array.isArray(e.projects)?e.projects[0]:null,r=n==null?void 0:n.posts;if(!r){Pt.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]];Pt.innerHTML=o.map(([t,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${t}</div></div>`).join("")}catch(e){Pt.innerHTML=`<div class="muted">Stats unavailable: ${(e==null?void 0:e.message)||e}</div>`}}function Xn(e,n,r){const o=e.textContent;e.disabled=!0,e.textContent=n,r().finally(()=>{e.disabled=!1,e.textContent=o,Jt()})}Xe.addEventListener("click",()=>Xn(Xe,"Starting…",async()=>{F("Asking Claude to run setup…");try{const e=await Ie.sendMessage({role:"user",content:[{type:"text",text:"Set up S4L plugin end to end now. Inspect and repair the runtime, auto-detect and connect my X session, scan my profile, discover and research my product, then infer and save a complete project with seeded search topics. Keep going without asking me to approve each safe setup step. Ask only if I must interactively sign in or no product can be identified. Keep every reply to me extremely concise: a few short sentences at most, no step-by-step narration or long status walls. If you must ask me something (e.g. the product URL), make it one short question."}]});e!=null&&e.isError?F("The host rejected the setup request — type “set up S4L” in the chat instead."):(F("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),ug())}catch(e){F("Couldn’t start setup: "+((e==null?void 0:e.message)||e))}}));Ht.addEventListener("click",()=>Xn(Ht,"Setting up…",async()=>{F("Asking Claude to schedule the draft tasks for this account…"),Y("client_event",{event:"rearm_clicked",surface:"panel"}).catch(()=>{});try{const e=await Ie.sendMessage({role:"user",content:[{type:"text",text:`Set up the S4L draft autopilot schedule for this Claude account. If queue_setup is available, call it; then for s4l-worker call the host tool create_scheduled_task with taskId, cronExpression "* * * * *", notifyOnCompletion false (REQUIRED — the default true pops a notification every minute), and the prompt — read it from ~/.claude/scheduled-tasks/s4l-worker/SKILL.md (already on disk). If the task already exists, call update_scheduled_task with taskId s4l-worker and notifyOnCompletion false instead. Before calling create_scheduled_task/list_scheduled_tasks/update_scheduled_task, run (via Bash, best-effort, ignore any error and continue if the path doesn't exist): python3 ~/.social-autoposter-mcp/repo/package/scripts/mark_event.py start <that tool's name>; immediately after it returns, run the same with 'end' instead of 'start'. Do NOT redo my X connection or project setup. Keep replies to me very short.`}]});e!=null&&e.isError?F("The host rejected it — type “set up the draft schedule” in the chat instead."):F("Scheduling is running in the chat. The draft tasks will register for this account.")}catch(e){F("Couldn’t start scheduling: "+((e==null?void 0:e.message)||e))}}));function Bl(e,n){e.addEventListener("click",async()=>{if(!e.disabled){e.disabled=!0,e.setAttribute("aria-checked",String(e.getAttribute("aria-checked")!=="true"));try{const r=await Y("engagement_mode",{action:"toggle",lane:n});r&&r.flags&&be({flags:r.flags}),await ge()}catch(r){F("Couldn’t switch lane: "+((r==null?void 0:r.message)||r)),await ge()}finally{e.disabled=!1}}})}Bl(Ml,"personal_brand");Bl(ql,"promotion");ve.addEventListener("click",async()=>{if(ve.disabled)return;const e=ve.getAttribute("aria-checked")!=="true",n=e?"resume":"pause";ve.disabled=!0,ve.setAttribute("aria-checked",String(e));try{const r=await Y("pause_s4l",{action:n});F(r!=null&&r.ok?n==="pause"?"S4L paused — drafting and posting are stopped.":"S4L resumed.":`Couldn’t ${n} S4L${r!=null&&r.detail?": "+r.detail:"."}`),await ge()}catch(r){F(`Couldn’t ${n} S4L: `+((r==null?void 0:r.message)||r)),await ge()}finally{ve.disabled=!1}});function Vl(e){eg.textContent=`${e}% personal brand / ${100-e}% promotion`}$e.addEventListener("pointerdown",()=>{Yn=!0});$e.addEventListener("input",()=>{Yn=!0,Vl(Number($e.value))});$e.addEventListener("change",async()=>{const e=Number($e.value);$e.disabled=!0;try{const n=await Y("engagement_mode",{action:"split",split:e});n&&typeof n.personal_brand_share=="number"&&be({personal_brand_share:n.personal_brand_share}),await ge()}catch(n){F("Couldn’t set the lane split: "+((n==null?void 0:n.message)||n)),await ge()}finally{$e.disabled=!1,Yn=!1}});let Wl=!1;function lg(e){if(!e||e.error)return;const n=e.mode||"";Ce.value=n;const r=Array.isArray(e.rates)?e.rates:[],o={high:"Aggressive",medium:"Steady",low:"Chill"};for(const t of r){const i=Ce.querySelector(`option[value="${t.mode}"]`);if(i&&t.est_posts_per_day!==null&&t.est_posts_per_day!==void 0){const a=Number(t.est_posts_per_day),s=o[t.mode]||t.mode;i.textContent=`${s} (~${a>=10?Math.round(a):a}/day)`}}tg.textContent=n?"How many drafts per day the cycle produces. Applies from the next cycle.":"How many drafts per day the cycle produces."}async function pr(){try{const e=await Y("posting_volume",{action:"get"});Wl=!0,lg(e)}catch{}}Ce.addEventListener("change",async()=>{const e=Ce.value||"default";Ce.disabled=!0;try{const n=await Y("posting_volume",{action:"set",mode:e});n&&n.error?F("Couldn’t change posting volume: "+n.error):F(e==="default"?"Posting volume back to the default cycle setting.":`Posting volume set to ${{high:"Aggressive",medium:"Steady",low:"Chill"}[e]||e}. Applies from the next cycle.`),await pr()}catch(n){F("Couldn’t change posting volume: "+((n==null?void 0:n.message)||n)),await pr()}finally{Ce.disabled=!1}});Ne.addEventListener("click",()=>{ke=!ke,mr()});er.addEventListener("click",()=>{Qe=!Qe,Pt.hidden=!Qe,er.setAttribute("aria-expanded",String(Qe)),er.classList.toggle("expanded",Qe)});Al.addEventListener("click",e=>{const n=e.target;n&&n.id==="btn-update"&&dg()});async function dg(){if(ir)return;ir=!0;const e=document.getElementById("btn-update");e&&(e.disabled=!0,e.textContent="Updating…"),F("Installing the latest release… this can take a minute.");try{const n=await Y("runtime",{action:"update"});n.ok?(F(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),e&&(e.textContent="Update installed — restart to apply")):(F("Update failed (exit "+(n.exit_code??"?")+"). Try `npx social-autoposter@latest update` in a terminal."),e&&(e.disabled=!1,e.textContent="Retry update"))}catch(n){F("Update failed: "+((n==null?void 0:n.message)||n)),e&&(e.disabled=!1,e.textContent="Retry update")}finally{ir=!1}}_e.addEventListener("click",async()=>{Ae.hidden=!0,_e.disabled=!0,_e.textContent="Starting…",F("Installing the runtime — this is a one-time download (~150MB+).");try{const e=await Y("runtime",{action:"install"});if(e.runtime_ready){be({runtime_ready:!0}),ge();return}To(e.progress??null),Po()}catch(e){_e.disabled=!1,_e.textContent="Retry install",Ae.textContent="Couldn't start install: "+((e==null?void 0:e.message)||e),Ae.hidden=!1}});aa.addEventListener("click",()=>Xn(aa,"Restarting…",async()=>{F("Restarting the S4L menu bar…");try{const e=await Y("restart_menubar");typeof e.menubar_running=="boolean"&&be({menubar_running:e.menubar_running}),F(e.menubar_running?"Menu bar restarted.":"Couldn’t confirm the menu bar came back"+(e.detail?": "+e.detail:"."))}catch(e){F("Couldn’t restart the menu bar: "+((e==null?void 0:e.message)||e))}}));const mg=["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"],pg=["description","voice","search_topics","content_angle","content_guardrails"],fr={website:"Website",description:"What it does",icp:"Target audience",voice:"Voice",differentiator:"Differentiator",search_topics:"Search topics",get_started_link:"Get-started link",content_guardrails:"Content guardrails",content_angle:"Content angle"},ca=new Set(["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"]),fg=new Set(["website","get_started_link"]);function et(e){const n=e.replace(/[_-]+/g," ").trim();return n?n.charAt(0).toUpperCase()+n.slice(1):e}function Kl(e){return!!e&&typeof e=="object"&&!Array.isArray(e)}function ua(e){return Array.isArray(e)&&e.every(n=>typeof n=="string")?{kind:"list",text:e.join(`
|
|
73
|
+
container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:d().optional().describe("User's language and region preference in BCP 47 format."),timeZone:d().optional().describe("User's timezone in IANA format."),userAgent:d().optional().describe("Host application identifier."),platform:U([u("web"),u("desktop"),u("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:m({touch:H().optional().describe("Whether the device supports touch input."),hover:H().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(),Ch=m({method:u("ui/notifications/host-context-changed"),params:Cl.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:x(jt).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:A(d(),J().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});m({method:u("ui/initialize"),params:m({appInfo:Kn.describe("App identification (name and version)."),appCapabilities:Uh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var Zh=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Kn.describe("Host application identification and version."),hostCapabilities:Ph.describe("Features and capabilities provided by the host."),hostContext:Cl.describe("Rich context about the host environment.")}).passthrough(),Ah={target:"draft-2020-12"};async function ta(e,n){let r=e["~standard"];if(r.jsonSchema)return r.jsonSchema[n](Ah);if(r.vendor==="zod"){let{z:o}=await td(()=>Promise.resolve().then(()=>zp),void 0,import.meta.url);return o.toJSONSchema(e,{io:n})}throw Error(`Schema (vendor: ${r.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function na(e,n,r=""){let o=await e["~standard"].validate(n);if(o.issues){let t=o.issues.map(i=>{var s;let a=(s=i.path)==null?void 0:s.map(c=>typeof c=="object"?c.key:c).join(".");return a?`${a}: ${i.message}`:i.message}).join("; ");throw Error(r+t)}return o.value}function Lh(e){let n=document.documentElement;n.setAttribute("data-theme",e),n.style.colorScheme=e}function Mh(e,n=document.documentElement){for(let[r,o]of Object.entries(e))o!==void 0&&n.style.setProperty(r,o)}function qh(e){if(document.getElementById("__mcp-host-fonts"))return;let n=document.createElement("style");n.id="__mcp-host-fonts",n.textContent=e,document.head.appendChild(n)}const Bt=class Bt extends gh{constructor(r,o={},t={autoResize:!0}){super(t);C(this,"_appInfo");C(this,"_capabilities");C(this,"options");C(this,"_hostCapabilities");C(this,"_hostInfo");C(this,"_hostContext");C(this,"_registeredTools",{});C(this,"_initializedSent",!1);C(this,"eventSchemas",{toolinput:zh,toolinputpartial:xh,toolresult:Rh,toolcancelled:jh,hostcontextchanged:Ch});C(this,"_everHadListener",new Set);C(this,"_toolHandlersInitialized",!1);C(this,"_onteardown");C(this,"_oncalltool");C(this,"_onlisttools");C(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=t,t.allowUnsafeEval||ee({jitless:!0}),this.setRequestHandler(Gn,i=>(console.log("Received ping:",i.params),{})),this.setEventHandler("hostcontextchanged",void 0)}_assertInitialized(r){var t;if(this._initializedSent)return;let o=`[ext-apps] App.${r}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if((t=this.options)!=null&&t.strict)throw Error(o);console.warn(`${o}. This will throw in a future release.`)}_assertHandlerTiming(r){var t;if(!Bt.ONE_SHOT_EVENTS.has(r)||this._everHadListener.has(r)||(this._everHadListener.add(r),!this._initializedSent))return;let o=`[ext-apps] "${String(r)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if((t=this.options)!=null&&t.strict)throw Error(o);console.warn(o)}setEventHandler(r,o){o&&this._assertHandlerTiming(r),super.setEventHandler(r,o)}addEventListener(r,o){this._assertHandlerTiming(r),super.addEventListener(r,o)}onEventDispatch(r,o){r==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...o})}registerCapabilities(r){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._capabilities=hh(this._capabilities,r)}registerTool(r,o,t){if(this._registeredTools[r])throw Error(`Tool ${r} is already registered`);let i=this,a=()=>{var p;i._initializedSent&&((p=i._capabilities.tools)!=null&&p.listChanged)&&i.sendToolListChanged()},s=o.inputSchema!==void 0,c={title:o.title,description:o.description,inputSchema:o.inputSchema,outputSchema:o.outputSchema,annotations:o.annotations,_meta:o._meta,enabled:!0,enable(){this.enabled=!0,a()},disable(){this.enabled=!1,a()},update(p){Object.assign(this,p),a()},remove(){i._registeredTools[r]===c&&(delete i._registeredTools[r],a())},handler:async(p,h)=>{if(!c.enabled)throw Error(`Tool ${r} is disabled`);let f;if(s){let _=c.inputSchema,w=_?await na(_,p??{},`Invalid input for tool ${r}: `):p??{};f=await t(w,h)}else f=await t(h);return c.outputSchema&&!f.isError&&(f.structuredContent=await na(c.outputSchema,f.structuredContent,`Invalid output for tool ${r}: `)),f}};return this._registeredTools[r]=c,!this._capabilities.tools&&!this.transport&&this.registerCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),a(),c}ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(r,o)=>{let t=this._registeredTools[r.name];if(!t)throw Error(`Tool ${r.name} not found`);return t.handler(r.arguments,o)},this.onlisttools=async(r,o)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([t,i])=>i.enabled).map(async([t,i])=>{let a={name:t,title:i.title,description:i.description,inputSchema:i.inputSchema?await ta(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await ta(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(Th,(o,t)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(o.params,t)})}get oncalltool(){return this._oncalltool}set oncalltool(r){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,r),this._oncalltool=r,this.replaceRequestHandler(Ul,(o,t)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(o.params,t)})}get onlisttools(){return this._onlisttools}set onlisttools(r){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,r),this._onlisttools=r,this.replaceRequestHandler(Pl,(o,t)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(o.params,t)})}assertCapabilityForMethod(r){var o;switch(r){case"sampling/createMessage":if(!((o=this._hostCapabilities)!=null&&o.sampling))throw Error(`Host does not support sampling (required for ${r})`);break}}assertRequestHandlerCapability(r){switch(r){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${r})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${r} registered`)}}assertNotificationCapability(r){}assertTaskCapability(r){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(r){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(r,o){if(this._assertInitialized("callServerTool"),typeof r=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${r}"). Did you mean: callServerTool({ name: "${r}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:r},Qn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},Nl,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},jl,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let t=r.tools?Rl:Dl;return await this.request({method:"sampling/createMessage",params:r},t,o)}sendMessage(r,o){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:r},Ih,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},lo,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},Sh,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},wh,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},Eh,o)}sendSizeChanged(r){return this.notification({method:"ui/notifications/size-changed",params:r})}setupSizeChangedNotifications(){let r=!1,o=0,t=0,i=()=>{r||(r=!0,requestAnimationFrame(()=>{r=!1;let s=document.documentElement,c=s.style.height;s.style.height="max-content";let p=Math.ceil(s.getBoundingClientRect().height);s.style.height=c;let h=Math.ceil(window.innerWidth);(h!==o||p!==t)&&(o=h,t=p,this.sendSizeChanged({width:h,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(r=new bh(window.parent,window.parent),o){var t;if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1,await super.connect(r);try{let i=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:vh}},Zh,o);if(i===void 0)throw Error(`Server sent invalid initialize result: ${i}`);this._hostCapabilities=i.hostCapabilities,this._hostInfo=i.hostInfo,this._hostContext=i.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,(t=this.options)!=null&&t.autoResize&&this.setupSizeChangedNotifications()}catch(i){throw this.close(),i}}};C(Bt,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let dr=Bt;function ra(e){const n=e&&e.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(e&&e.content||[]).find(o=>o&&o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{}}return{}}class Hh{constructor(){C(this,"onhostcontextchanged");C(this,"onerror");C(this,"ontoolresult")}async connect(){var n,r;try{const[o,t]=await Promise.all([this.callServerTool({name:"project_config",arguments:{status:!0}}),this.callServerTool({name:"runtime",arguments:{action:"status"}})]),i=ra(o),a=ra(t),s=Array.isArray(i.projects)?i.projects:[],c={projects:s,projects_total:s.length,projects_ready:s.filter(p=>p&&p.ready).length,x_connected:!!i.x_connected,x_state:i.x_state||"",x_handle:i.x_handle??null,version:i.mcp_version||"",latest_version:i.latest_version??null,update_available:!!i.update_available,runtime_ready:typeof a.runtime_ready=="boolean"?a.runtime_ready:!0,runtime_provisioning:!!a.provisioning,onboarding:a.onboarding||i.onboarding};(n=this.ontoolresult)==null||n.call(this,{structuredContent:{snapshot:JSON.stringify(c)}})}catch(o){(r=this.onerror)==null||r.call(this,o)}}getHostContext(){}async callServerTool(n){const r=await fetch(`/tool/${encodeURIComponent(n.name)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n.arguments??{})});if(!r.ok){let o=`HTTP ${r.status}`;try{o=await r.text()||o}catch{}return{isError:!0,content:[{type:"text",text:o}]}}return r.json()}async sendMessage(){return{isError:!0}}}function Fh(){return globalThis.__S4L_BRIDGE__==="http"?new Hh:new dr({name:"S4L Panel",version:"1.0.0"})}function Zl(e){const n=e.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(e.content||[]).find(o=>o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{_raw:r.text}}return{}}const D=e=>document.getElementById(e),Al=D("ver"),Xe=D("btn-setup"),Ht=D("btn-schedule"),Pt=D("stats-grid"),er=D("stats-toggle"),Jh=D("log"),Bh=D("install-card"),Ne=D("setup-summary"),Ll=D("onboarding-details"),Vh=D("onboarding-steps"),tr=D("onboarding-blocker"),ia=D("onboarding-count"),Wh=D("onboarding-bar-fill"),Kh=D("live-card"),Gh=D("stats-card"),oa=D("install-steps"),Ae=D("install-err"),_e=D("btn-install"),Qh=D("menubar-banner"),aa=D("btn-menubar-restart"),No=D("btn-live"),Oo=D("btn-live-stop"),sa=D("btn-live-front"),Le=D("live-status"),Ft=D("live-img"),Ml=D("switch-personal"),ql=D("switch-promo"),ve=D("switch-pause"),Yh=D("pause-name"),Xh=D("split-row"),eg=D("split-desc"),$e=D("split-slider"),Ce=D("posting-volume-select"),tg=D("posting-volume-desc"),ng=D("mode-sub"),rg=D("settings-card"),nr=D("settings-toggle"),fe=D("settings-body");let E=null,rr=!1,Ut=!1,ir=!1,ke=!1,Yn=!1,Qe=!1,De=!1,or=!1;function F(e){Jh.textContent=e}function ig(e){switch(e){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function To(e){if(!e||!Array.isArray(e.steps)){oa.innerHTML="";return}oa.innerHTML=e.steps.map(n=>{const r=n.detail&&n.status!=="pending"?` <span class="detail">${n.status==="error"?n.detail:""}</span>`:"";return`<li class="${n.status}"><span class="glyph">${ig(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),e.error?(Ae.textContent=e.error,Ae.hidden=!1):Ae.hidden=!0}const og={environment_checked:"Environment checked",runtime_ready:"Runtime ready",x_connected:"X connected",profile_scanned:"Profile scanned",project_ready:"Project ready",topics_seeded:"Topics seeded",tasks_scheduled:"Tasks scheduled",reddit_connected:"Reddit connected",reddit_verified:"Reddit verified"};function ag(e){switch(e){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function mr(){Ll.hidden=!ke,Ne.setAttribute("aria-expanded",String(ke)),Ne.classList.toggle("expanded",ke)}function Hl(e){return e?e.setup_complete!==void 0?!!e.setup_complete:!!e.runtime_ready&&(e.projects_ready||0)>0&&!!e.x_connected:!1}function sg(e){if(!e||!Array.isArray(e.milestones)){Ne.hidden=!0,Ll.hidden=!0;return}const n=Hl(E);Ne.hidden=n,n&&ke&&(ke=!1,mr());const r=e.milestones.length,o=e.milestones.filter(i=>i.status==="complete").length,t=!!e.current_blocker&&!n;Ne.classList.toggle("complete",n),Ne.classList.toggle("blocked",t),ia.hidden=n,ia.textContent=t?`${o}/${r} · needs you`:Ut?`${o}/${r} · setting up…`:`${o}/${r}`,Wh.style.width=r>0?`${Math.round(o/r*100)}%`:"0%",Vh.innerHTML=e.milestones.map(i=>{const a=og[i.id]||i.id,s=i.attempts>1?` <span class="detail">${i.attempts} attempts</span>`:"";return`<li class="${i.status}"><span class="glyph">${ag(i.status)}</span><span>${a}${s}</span></li>`}).join(""),e.current_blocker?(tr.textContent=`Current blocker: ${e.current_blocker.message}`,tr.hidden=!1,ke=!0):tr.hidden=!0,mr()}function Jt(){if(!E)return;sg(E.onboarding),Al.innerHTML=E.update_available&&E.latest_version?`v${E.version} · <button id="btn-update" class="update-btn">Update to ${E.latest_version}</button>`:`v${E.version}`;const e=!E.runtime_ready;Bh.hidden=!e,Qh.hidden=e||E.menubar_running!==!1;const n=Hl(E);Xe.hidden=n,Xe.disabled=!1,Xe.classList.toggle("primary",!n);const r=n&&(E.schedule_state==="missing"||E.schedule_state==="disabled"||E.schedule_state==="stalled");Ht.hidden=!r,Ht.classList.toggle("primary",r);const o=E.flags||(E.mode==="promotion"?{personal_brand:!1,promotion:!0}:{personal_brand:!0,promotion:!1}),t=!!o.personal_brand,i=!!o.promotion;Ml.setAttribute("aria-checked",String(t)),ql.setAttribute("aria-checked",String(i));const a=Math.round((typeof E.personal_brand_share=="number"?E.personal_brand_share:.5)*100);Xh.hidden=!(t&&i),Yn||($e.value=String(a),Vl(a)),ng.textContent=t&&i?`Both lanes on: cycles split ${a}/${100-a} personal/promotion.`:!t&&!i?"No lane on; the cycle falls back to personal brand.":"";const s=E.paused===!0;ve.setAttribute("aria-checked",String(!s)),ve.setAttribute("aria-label",s?"Resume S4L":"Pause S4L"),ve.title=s?"Resume S4L":"Pause S4L",Yh.textContent=s?"Paused":"Running",Kh.hidden=!n,Gh.hidden=!n,rg.hidden=e}function be(e){E={...E||{},...e},Jt()}function cg(e){const n=Array.isArray(e.projects)?e.projects:[];return{projects:n,projects_total:n.length,projects_ready:n.filter(r=>r.ready).length,x_connected:!!e.x_connected,x_state:e.x_state||"",x_handle:e.x_handle??null,...e.setup_complete!==void 0?{setup_complete:!!e.setup_complete}:{},version:e.mcp_version||(E==null?void 0:E.version)||"",latest_version:e.latest_version??null,update_available:!!e.update_available,mode:e.mode??(E==null?void 0:E.mode),flags:e.flags??(E==null?void 0:E.flags),personal_brand_share:e.personal_brand_share??(E==null?void 0:E.personal_brand_share),onboarding:e.onboarding}}const Ie=Fh();function Fl(e){var n,r,o;e.theme&&Lh(e.theme),(n=e.styles)!=null&&n.variables&&Mh(e.styles.variables),(o=(r=e.styles)==null?void 0:r.css)!=null&&o.fonts&&qh(e.styles.css.fonts)}Ie.onhostcontextchanged=Fl;Ie.onerror=e=>console.error(e);Ie.ontoolresult=e=>{const n=Zl(e);n&&typeof n.projects_total=="number"&&(be(n),n.runtime_ready?Jl():n.runtime_provisioning&&Po())};async function Y(e,n={}){const r=await Ie.callServerTool({name:e,arguments:n});return Zl(r)}async function ge(){F("Refreshing…");try{const[e,n]=await Promise.all([Y("project_config",{status:!0}),Y("runtime",{action:"status"}).catch(()=>({}))]);be({...cg(e),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},...typeof n.menubar_running=="boolean"?{menubar_running:n.menubar_running}:{},...typeof n.paused=="boolean"?{paused:n.paused}:{},onboarding:n.onboarding||e.onboarding||(E==null?void 0:E.onboarding)}),E&&!E.runtime_ready&&n.provisioning&&Po(),F(""),Jl()}catch(e){F("Refresh failed: "+((e==null?void 0:e.message)||e))}}async function Po(){if(!rr){rr=!0,_e.disabled=!0,_e.textContent="Installing…";try{for(;;){const e=await Y("runtime",{action:"status"}).catch(()=>({}));if(To(e.progress??null),e.onboarding&&be({onboarding:e.onboarding}),e.runtime_ready){be({runtime_ready:!0}),F("Runtime installed; you're ready to set up."),ge();return}const n=e.progress??null;if(n&&n.done&&!n.ok){_e.disabled=!1,_e.textContent="Retry install",F("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{rr=!1}}}async function ug(){var r;if(Ut)return;Ut=!0,Jt();const e=Date.now(),n=1200*1e3;try{for(;;){const o=await Y("runtime",{action:"status"}).catch(()=>({}));o.progress&&To(o.progress);const t={};if(typeof o.runtime_ready=="boolean"&&(t.runtime_ready=o.runtime_ready),o.onboarding&&(t.onboarding=o.onboarding),Object.keys(t).length&&be(t),(r=o.onboarding)!=null&&r.complete){await ge(),F("Setup complete.");break}if(Date.now()-e>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{Ut=!1,Jt()}}async function Jl(){try{const e=await Y("get_stats",{days:7}),n=Array.isArray(e.projects)?e.projects[0]:null,r=n==null?void 0:n.posts;if(!r){Pt.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]];Pt.innerHTML=o.map(([t,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${t}</div></div>`).join("")}catch(e){Pt.innerHTML=`<div class="muted">Stats unavailable: ${(e==null?void 0:e.message)||e}</div>`}}function Xn(e,n,r){const o=e.textContent;e.disabled=!0,e.textContent=n,r().finally(()=>{e.disabled=!1,e.textContent=o,Jt()})}Xe.addEventListener("click",()=>Xn(Xe,"Starting…",async()=>{F("Asking Claude to run setup…");try{const e=await Ie.sendMessage({role:"user",content:[{type:"text",text:"Set up S4L plugin end to end now. Inspect and repair the runtime, auto-detect and connect my X session, scan my profile, discover and research my product, then infer and save a complete project with seeded search topics. Keep going without asking me to approve each safe setup step. Ask only if I must interactively sign in or no product can be identified. Keep every reply to me extremely concise: a few short sentences at most, no step-by-step narration or long status walls. If you must ask me something (e.g. the product URL), make it one short question."}]});e!=null&&e.isError?F("The host rejected the setup request — type “set up S4L” in the chat instead."):(F("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),ug())}catch(e){F("Couldn’t start setup: "+((e==null?void 0:e.message)||e))}}));Ht.addEventListener("click",()=>Xn(Ht,"Setting up…",async()=>{F("Asking Claude to schedule the draft tasks for this account…"),Y("client_event",{event:"rearm_clicked",surface:"panel"}).catch(()=>{});try{const e=await Ie.sendMessage({role:"user",content:[{type:"text",text:`Set up the S4L draft autopilot schedule for this Claude account. If queue_setup is available, call it; then for s4l-worker call the host tool create_scheduled_task with taskId, cronExpression "* * * * *", notifyOnCompletion false (REQUIRED — the default true pops a notification every minute), and the prompt — read it from ~/.claude/scheduled-tasks/s4l-worker/SKILL.md (already on disk). If the task already exists, call update_scheduled_task with taskId s4l-worker and notifyOnCompletion false instead. Before calling create_scheduled_task/list_scheduled_tasks/update_scheduled_task, run (via Bash, best-effort, ignore any error and continue if the path doesn't exist): python3 ~/.social-autoposter-mcp/repo/package/scripts/mark_event.py start <that tool's name>; immediately after it returns, run the same with 'end' instead of 'start'. Do NOT redo my X connection or project setup. Keep replies to me very short.`}]});e!=null&&e.isError?F("The host rejected it — type “set up the draft schedule” in the chat instead."):F("Scheduling is running in the chat. The draft tasks will register for this account.")}catch(e){F("Couldn’t start scheduling: "+((e==null?void 0:e.message)||e))}}));function Bl(e,n){e.addEventListener("click",async()=>{if(!e.disabled){e.disabled=!0,e.setAttribute("aria-checked",String(e.getAttribute("aria-checked")!=="true"));try{const r=await Y("engagement_mode",{action:"toggle",lane:n});r&&r.flags&&be({flags:r.flags}),await ge()}catch(r){F("Couldn’t switch lane: "+((r==null?void 0:r.message)||r)),await ge()}finally{e.disabled=!1}}})}Bl(Ml,"personal_brand");Bl(ql,"promotion");ve.addEventListener("click",async()=>{if(ve.disabled)return;const e=ve.getAttribute("aria-checked")!=="true",n=e?"resume":"pause";ve.disabled=!0,ve.setAttribute("aria-checked",String(e));try{const r=await Y("pause_s4l",{action:n});F(r!=null&&r.ok?n==="pause"?"S4L paused — drafting and posting are stopped.":"S4L resumed.":`Couldn’t ${n} S4L${r!=null&&r.detail?": "+r.detail:"."}`),await ge()}catch(r){F(`Couldn’t ${n} S4L: `+((r==null?void 0:r.message)||r)),await ge()}finally{ve.disabled=!1}});function Vl(e){eg.textContent=`${e}% personal brand / ${100-e}% promotion`}$e.addEventListener("pointerdown",()=>{Yn=!0});$e.addEventListener("input",()=>{Yn=!0,Vl(Number($e.value))});$e.addEventListener("change",async()=>{const e=Number($e.value);$e.disabled=!0;try{const n=await Y("engagement_mode",{action:"split",split:e});n&&typeof n.personal_brand_share=="number"&&be({personal_brand_share:n.personal_brand_share}),await ge()}catch(n){F("Couldn’t set the lane split: "+((n==null?void 0:n.message)||n)),await ge()}finally{$e.disabled=!1,Yn=!1}});let Wl=!1;function lg(e){if(!e||e.error)return;const n=e.mode||"medium";Ce.value=n;const r=Array.isArray(e.rates)?e.rates:[],o={high:"Aggressive",medium:"Steady",low:"Chill"};for(const t of r){const i=Ce.querySelector(`option[value="${t.mode}"]`);if(i&&t.est_posts_per_day!==null&&t.est_posts_per_day!==void 0){const a=Number(t.est_posts_per_day),s=o[t.mode]||t.mode;i.textContent=`${s} (~${a>=10?Math.round(a):a}/day)`}}tg.textContent="How many drafts per day the cycle produces."}async function pr(){try{const e=await Y("posting_volume",{action:"get"});Wl=!0,lg(e)}catch{}}Ce.addEventListener("change",async()=>{const e=Ce.value;Ce.disabled=!0;try{const n=await Y("posting_volume",{action:"set",mode:e});n&&n.error?F("Couldn’t change posting volume: "+n.error):F(`Posting volume set to ${{high:"Aggressive",medium:"Steady",low:"Chill"}[e]||e}. Applies from the next cycle.`),await pr()}catch(n){F("Couldn’t change posting volume: "+((n==null?void 0:n.message)||n)),await pr()}finally{Ce.disabled=!1}});Ne.addEventListener("click",()=>{ke=!ke,mr()});er.addEventListener("click",()=>{Qe=!Qe,Pt.hidden=!Qe,er.setAttribute("aria-expanded",String(Qe)),er.classList.toggle("expanded",Qe)});Al.addEventListener("click",e=>{const n=e.target;n&&n.id==="btn-update"&&dg()});async function dg(){if(ir)return;ir=!0;const e=document.getElementById("btn-update");e&&(e.disabled=!0,e.textContent="Updating…"),F("Installing the latest release… this can take a minute.");try{const n=await Y("runtime",{action:"update"});n.ok?(F(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),e&&(e.textContent="Update installed — restart to apply")):(F("Update failed (exit "+(n.exit_code??"?")+"). Try `npx social-autoposter@latest update` in a terminal."),e&&(e.disabled=!1,e.textContent="Retry update"))}catch(n){F("Update failed: "+((n==null?void 0:n.message)||n)),e&&(e.disabled=!1,e.textContent="Retry update")}finally{ir=!1}}_e.addEventListener("click",async()=>{Ae.hidden=!0,_e.disabled=!0,_e.textContent="Starting…",F("Installing the runtime — this is a one-time download (~150MB+).");try{const e=await Y("runtime",{action:"install"});if(e.runtime_ready){be({runtime_ready:!0}),ge();return}To(e.progress??null),Po()}catch(e){_e.disabled=!1,_e.textContent="Retry install",Ae.textContent="Couldn't start install: "+((e==null?void 0:e.message)||e),Ae.hidden=!1}});aa.addEventListener("click",()=>Xn(aa,"Restarting…",async()=>{F("Restarting the S4L menu bar…");try{const e=await Y("restart_menubar");typeof e.menubar_running=="boolean"&&be({menubar_running:e.menubar_running}),F(e.menubar_running?"Menu bar restarted.":"Couldn’t confirm the menu bar came back"+(e.detail?": "+e.detail:"."))}catch(e){F("Couldn’t restart the menu bar: "+((e==null?void 0:e.message)||e))}}));const mg=["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"],pg=["description","voice","search_topics","content_angle","content_guardrails"],fr={website:"Website",description:"What it does",icp:"Target audience",voice:"Voice",differentiator:"Differentiator",search_topics:"Search topics",get_started_link:"Get-started link",content_guardrails:"Content guardrails",content_angle:"Content angle"},ca=new Set(["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"]),fg=new Set(["website","get_started_link"]);function et(e){const n=e.replace(/[_-]+/g," ").trim();return n?n.charAt(0).toUpperCase()+n.slice(1):e}function Kl(e){return!!e&&typeof e=="object"&&!Array.isArray(e)}function ua(e){return Array.isArray(e)&&e.every(n=>typeof n=="string")?{kind:"list",text:e.join(`
|
|
74
74
|
`)}:e==null||typeof e=="string"?{kind:"text",text:String(e??"")}:{kind:"json",text:JSON.stringify(e,null,2)}}function la(e,n,r){const o=document.createElement("div");o.className="settings-field";const t=document.createElement("label");t.textContent=e+(n.kind==="list"?" (one per line)":""),o.appendChild(t);let i;if(n.kind==="text"&&r)i=document.createElement("input"),i.type="text",i.className="settings-input";else{const a=document.createElement("textarea"),s=n.text?n.text.split(`
|
|
75
75
|
`).reduce((c,p)=>c+Math.max(1,Math.ceil(p.length/60)),0):1;a.rows=Math.min(10,Math.max(2,s)),a.className="settings-textarea"+(n.kind==="json"?" mono":""),i=a}return i.value=n.text,n.text||(i.placeholder="Not set"),i.dataset.orig=n.text,o.appendChild(i),{wrap:o,el:i}}function hg(e){const n=document.createElement("div");n.className="settings-project";const r=document.createElement("div");r.className="settings-project-head";const o=document.createElement("span");if(o.className="settings-project-name",o.textContent=e.name,r.appendChild(o),e.persona){const f=document.createElement("span");f.className="settings-project-tag",f.textContent="personal brand",r.appendChild(f)}const t=document.createElement("span");t.className="settings-project-state",t.textContent=e.ready?"ready":"missing: "+e.missing_required.join(", "),r.appendChild(t),n.appendChild(r);const i=[],a=e.persona?pg:mg;for(const f of a){const _=e.fields[f];if(Kl(_)&&Object.keys(_).length){const z=document.createElement("fieldset");z.className="settings-group";const S=document.createElement("legend");S.textContent=fr[f]||et(f),z.appendChild(S);for(const[g,$]of Object.entries(_)){const y=ua($),I=la(et(g),y,!1);z.appendChild(I.wrap),i.push({key:f,sub:g,kind:y.kind,el:I.el,orig:y.text})}n.appendChild(z);continue}const w=f==="search_topics"?{kind:"list",text:Array.isArray(_)?_.map(String).join(`
|
|
76
76
|
`):String(_??"")}:ua(_),b=la(fr[f]||et(f),w,fg.has(f));n.appendChild(b.wrap),i.push({key:f,kind:w.kind,el:b.el,orig:w.text})}if(e.extra_keys.length){const f=document.createElement("div");f.className="settings-extra",f.textContent="Advanced (edit via chat): "+e.extra_keys.join(", "),n.appendChild(f)}const s=document.createElement("div");s.className="settings-actions";const c=document.createElement("button");c.className="primary",c.textContent="Save changes",c.disabled=!0;const p=document.createElement("span");p.className="settings-status",s.appendChild(c),s.appendChild(p),n.appendChild(s);const h=()=>{c.disabled=!i.some(f=>f.el.value!==f.orig)};for(const f of i)f.el.addEventListener("input",h);return c.addEventListener("click",()=>void gg(e,i,c,p)),n}async function gg(e,n,r,o){const t=n.filter(h=>h.el.value!==h.orig);if(!t.length)return;const i={name:e.name},a={},s=h=>(fr[h.key]||et(h.key))+(h.sub?` · ${et(h.sub)}`:""),c=[...new Set(t.filter(h=>h.sub!==void 0).map(h=>h.key))];for(const h of c){const f=Kl(e.fields[h])?{...e.fields[h]}:{};for(const _ of n.filter(w=>w.key===h&&w.sub!==void 0)){const w=_.el.value;if(_.kind==="list"){const b=w.split(`
|
|
@@ -190,9 +190,8 @@ container holding the app. Specify either width or maxWidth, and either height o
|
|
|
190
190
|
<div class="lane-desc" id="posting-volume-desc">How many drafts per day the cycle produces.</div>
|
|
191
191
|
</div>
|
|
192
192
|
<select id="posting-volume-select" aria-label="Posting volume">
|
|
193
|
-
<option value="">Default</option>
|
|
194
193
|
<option value="high">Aggressive</option>
|
|
195
|
-
<option value="medium">Steady</option>
|
|
194
|
+
<option value="medium" selected>Steady</option>
|
|
196
195
|
<option value="low">Chill</option>
|
|
197
196
|
</select>
|
|
198
197
|
</div>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Reddit session bootstrap for the setup flow (mirrors twitterAuth.ts).
|
|
2
|
+
//
|
|
3
|
+
// Thin wrapper over scripts/setup_reddit_auth.py, which owns the real work
|
|
4
|
+
// (ensure the managed reddit-harness Chrome on CDP 9557, validate the
|
|
5
|
+
// reddit.com session via api/me.json fetched same-origin from inside a
|
|
6
|
+
// logged-in page, and, if logged out, import reddit.com cookies from the
|
|
7
|
+
// user's everyday browser via the vendored copy_browser_cookies.py). We only
|
|
8
|
+
// shell out and parse JSON.
|
|
9
|
+
//
|
|
10
|
+
// Why a separate Python helper instead of doing CDP here: the cookie-import
|
|
11
|
+
// and browser-fetch primitives already exist and are battle-tested in the
|
|
12
|
+
// repo (copy_browser_cookies.py, reddit_browser_fetch.py). Reusing them keeps
|
|
13
|
+
// this MCP a thin client and keeps the reddit-specific launch-flag parity
|
|
14
|
+
// (NO --use-mock-keychain on the reddit-harness profile) in one place.
|
|
15
|
+
import { runPython } from "./repo.js";
|
|
16
|
+
import { captureError } from "./telemetry.js";
|
|
17
|
+
function parse(stdout, stderr, code) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(stdout.trim().split("\n").slice(-80).join("\n"));
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
captureError(e, { component: "reddit_auth", phase: "parse", exit: String(code) });
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
connected: false,
|
|
26
|
+
state: "error",
|
|
27
|
+
error: `setup_reddit_auth.py produced no parseable JSON (exit ${code}).\n` +
|
|
28
|
+
(stderr || stdout).split("\n").slice(-8).join("\n"),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Probe-only: is the managed reddit session valid right now? Does NOT launch
|
|
33
|
+
// Chrome. Falls back to the on-disk profile-cookie check (connected_idle) when
|
|
34
|
+
// the harness is down, and short-circuits while a posting drain owns the tab.
|
|
35
|
+
export async function redditStatus() {
|
|
36
|
+
const res = await runPython("scripts/setup_reddit_auth.py", ["status"], {
|
|
37
|
+
timeoutMs: 90_000,
|
|
38
|
+
});
|
|
39
|
+
return parse(res.stdout, res.stderr, res.code);
|
|
40
|
+
}
|
|
41
|
+
// Ensure the harness browser is up, validate, and import reddit.com cookies
|
|
42
|
+
// from the user's everyday browser if needed. `source` optional (e.g.
|
|
43
|
+
// "arc:Default"); default auto-detects chrome/arc/brave/edge.
|
|
44
|
+
export async function redditConnect(source, manualLogin) {
|
|
45
|
+
const args = ["connect"];
|
|
46
|
+
if (source)
|
|
47
|
+
args.push("--source", source);
|
|
48
|
+
// Only pop a visible Reddit login window when the user explicitly asked to
|
|
49
|
+
// sign in by hand (mirrors connect_x's no-surprise-window discipline).
|
|
50
|
+
if (manualLogin)
|
|
51
|
+
args.push("--manual-login");
|
|
52
|
+
const res = await runPython("scripts/setup_reddit_auth.py", args, {
|
|
53
|
+
// Import may pop a macOS Keychain dialog the user has to find + click, and
|
|
54
|
+
// the manual-login wait is up to 300s. Keep this above the Python-side
|
|
55
|
+
// cookie-copy timeout (S4L_COOKIE_COPY_TIMEOUT, default 600s).
|
|
56
|
+
timeoutMs: 660_000,
|
|
57
|
+
});
|
|
58
|
+
return parse(res.stdout, res.stderr, res.code);
|
|
59
|
+
}
|
|
60
|
+
// List browsers/profiles to import from. Read-only: never reads the keychain
|
|
61
|
+
// or decrypts a cookie, so it shows no macOS Safe Storage prompt.
|
|
62
|
+
export async function redditDetectSources() {
|
|
63
|
+
const res = await runPython("scripts/setup_reddit_auth.py", ["detect-sources"], {
|
|
64
|
+
timeoutMs: 30_000,
|
|
65
|
+
});
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(res.stdout.trim().split("\n").slice(-200).join("\n"));
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
captureError(e, {
|
|
71
|
+
component: "reddit_auth",
|
|
72
|
+
phase: "detect_sources",
|
|
73
|
+
exit: String(res.code),
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
sources: [],
|
|
78
|
+
error: `detect-sources produced no parseable JSON (exit ${res.code}).\n` +
|
|
79
|
+
(res.stderr || res.stdout).split("\n").slice(-8).join("\n"),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// One-line human summary for tool output.
|
|
84
|
+
export function summarizeRedditAuth(r) {
|
|
85
|
+
switch (r.state) {
|
|
86
|
+
case "connected":
|
|
87
|
+
return ("Reddit is connected" +
|
|
88
|
+
(r.username ? ` as u/${r.username}` : "") +
|
|
89
|
+
" (the autoposter's reddit browser has a valid session).");
|
|
90
|
+
case "connected_idle":
|
|
91
|
+
return ("Reddit is connected (your session is saved in the autoposter's own browser " +
|
|
92
|
+
"profile). The reddit browser isn't running this moment; the next pipeline " +
|
|
93
|
+
"run restores it automatically, no action needed.");
|
|
94
|
+
case "imported":
|
|
95
|
+
return (`Reddit connected${r.username ? ` as u/${r.username}` : ""}; imported your ` +
|
|
96
|
+
`session from ${r.source ?? "your browser"}.`);
|
|
97
|
+
case "logged_out":
|
|
98
|
+
return "Reddit is not connected: the autoposter's reddit browser has no valid session yet.";
|
|
99
|
+
case "browser_not_running":
|
|
100
|
+
return "The autoposter's reddit browser isn't running yet.";
|
|
101
|
+
case "suspended":
|
|
102
|
+
return (`The reddit account${r.username ? ` u/${r.username}` : ""} is suspended; ` +
|
|
103
|
+
"posting is not possible. Connect a different account or resolve the suspension.");
|
|
104
|
+
case "needs_login":
|
|
105
|
+
return (r.note ??
|
|
106
|
+
"Couldn't import a valid Reddit session automatically. Sign in yourself in the " +
|
|
107
|
+
"autoposter's Chrome window at the Reddit login page, then run connect_reddit again.");
|
|
108
|
+
case "keychain_locked":
|
|
109
|
+
return r.note ?? "Cookie import blocked: the macOS keychain is not accessible from this session.";
|
|
110
|
+
case "browser_launch_failed":
|
|
111
|
+
return r.error ?? "Could not start the autoposter's reddit browser.";
|
|
112
|
+
default:
|
|
113
|
+
return r.error ?? `Reddit auth state: ${r.state}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
package/mcp/dist/runtime.js
CHANGED
|
@@ -59,6 +59,9 @@ const RUNTIME_DIR = path.join(STATE_DIR, "runtime");
|
|
|
59
59
|
const VENV_DIR = path.join(RUNTIME_DIR, ".venv");
|
|
60
60
|
const RUNTIME_JSON = path.join(STATE_DIR, "runtime.json");
|
|
61
61
|
const PROGRESS_JSON = path.join(STATE_DIR, "install-progress.json");
|
|
62
|
+
// Cross-process provisioning lock (see tryAcquireProvisionLock below).
|
|
63
|
+
const PROVISION_LOCK_PATH = path.join(STATE_DIR, "provision.lock");
|
|
64
|
+
const PROVISION_LOCK_STALE_MS = 60_000;
|
|
62
65
|
// The venv's interpreter, by absolute path (no activation needed).
|
|
63
66
|
const VENV_PYTHON = process.platform === "win32"
|
|
64
67
|
? path.join(VENV_DIR, "Scripts", "python.exe")
|
|
@@ -527,24 +530,96 @@ function findUv() {
|
|
|
527
530
|
}
|
|
528
531
|
// ---------------------------------------------------------------------------
|
|
529
532
|
// Provisioning. Idempotent: re-running re-derives any missing piece. A module
|
|
530
|
-
// guard prevents two concurrent runs within the same process
|
|
533
|
+
// guard (`inFlight`) prevents two concurrent runs within the same process,
|
|
534
|
+
// and a lock FILE (below) prevents two concurrent runs across processes.
|
|
531
535
|
// ---------------------------------------------------------------------------
|
|
532
536
|
let inFlight = null;
|
|
533
537
|
export function isProvisioning() {
|
|
534
538
|
return inFlight !== null;
|
|
535
539
|
}
|
|
540
|
+
// Cross-process guard. `inFlight` above only protects against two calls in
|
|
541
|
+
// the SAME process, but a new MCP server process spawns on every host boot —
|
|
542
|
+
// including every leftover/queue-worker scheduled-task tick — so two separate
|
|
543
|
+
// processes could both call startProvisioning() and race on the shared
|
|
544
|
+
// HARNESS_DIR checkout (one process's "delete broken checkout" cleanup
|
|
545
|
+
// colliding with another's `uv tool install`, corrupting the install; seen
|
|
546
|
+
// live on the MacStadium QA box 2026-07-15). Reuses the same wx-create +
|
|
547
|
+
// mtime-staleness idiom as version.ts's tryProbeLock. The holder refreshes
|
|
548
|
+
// the lock's mtime via the existing progress heartbeat so staleness detection
|
|
549
|
+
// stays accurate across long-running steps (Chromium/Chrome downloads).
|
|
550
|
+
function tryAcquireProvisionLock() {
|
|
551
|
+
const create = () => fs.closeSync(fs.openSync(PROVISION_LOCK_PATH, "wx"));
|
|
552
|
+
try {
|
|
553
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
554
|
+
create();
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
catch (e) {
|
|
558
|
+
if (e.code === "EEXIST") {
|
|
559
|
+
try {
|
|
560
|
+
if (Date.now() - fs.statSync(PROVISION_LOCK_PATH).mtimeMs > PROVISION_LOCK_STALE_MS) {
|
|
561
|
+
fs.unlinkSync(PROVISION_LOCK_PATH); // stale: holder died mid-provision
|
|
562
|
+
create();
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
/* raced with the holder finishing/cleaning up */
|
|
568
|
+
}
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
return true; // unexpected fs error — probe anyway rather than going blind
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function touchProvisionLock() {
|
|
575
|
+
try {
|
|
576
|
+
const now = new Date();
|
|
577
|
+
fs.utimesSync(PROVISION_LOCK_PATH, now, now);
|
|
578
|
+
}
|
|
579
|
+
catch {
|
|
580
|
+
/* lock may have been reclaimed as stale, or already released; ignore */
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function releaseProvisionLock() {
|
|
584
|
+
try {
|
|
585
|
+
fs.unlinkSync(PROVISION_LOCK_PATH);
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
/* already gone */
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// True when a DIFFERENT process holds a fresh (non-stale) provisioning lock.
|
|
592
|
+
// Used by boot-time actors other than provision() itself (e.g. the harness
|
|
593
|
+
// patch step) that also touch HARNESS_DIR and must not run concurrently with it.
|
|
594
|
+
export function anotherProcessProvisioning() {
|
|
595
|
+
try {
|
|
596
|
+
return (fs.existsSync(PROVISION_LOCK_PATH) &&
|
|
597
|
+
Date.now() - fs.statSync(PROVISION_LOCK_PATH).mtimeMs <= PROVISION_LOCK_STALE_MS);
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
536
603
|
// Kick off provisioning (or return the in-flight run). Returns immediately with
|
|
537
604
|
// the initial progress snapshot; the panel polls install_status for updates.
|
|
538
605
|
export function startProvisioning() {
|
|
539
606
|
if (!inFlight) {
|
|
607
|
+
if (!tryAcquireProvisionLock()) {
|
|
608
|
+
// A different process already holds a fresh lock and is actively
|
|
609
|
+
// provisioning — don't start a second run that would race on the same
|
|
610
|
+
// HARNESS_DIR checkout. Report whatever that process has written so far.
|
|
611
|
+
return readProgress() ?? freshProgress();
|
|
612
|
+
}
|
|
540
613
|
const progress = freshProgress();
|
|
541
614
|
writeProgress(progress);
|
|
542
615
|
// Heartbeat: the long download steps (Chromium ~150MB, Google Chrome DMG)
|
|
543
616
|
// don't call setStep for minutes, so the progress file's updated_at froze
|
|
544
617
|
// and pollers couldn't tell "slow" from "hung." Re-stamp updated_at on a
|
|
545
|
-
// timer while the run is live so status always shows real motion.
|
|
618
|
+
// timer while the run is live so status always shows real motion. Also
|
|
619
|
+
// refreshes the provision lock so its staleness check stays accurate.
|
|
546
620
|
const heartbeat = setInterval(() => {
|
|
547
621
|
try {
|
|
622
|
+
touchProvisionLock();
|
|
548
623
|
if (!progress.done)
|
|
549
624
|
writeProgress(progress);
|
|
550
625
|
}
|
|
@@ -556,6 +631,7 @@ export function startProvisioning() {
|
|
|
556
631
|
heartbeat.unref();
|
|
557
632
|
inFlight = provision(progress).finally(() => {
|
|
558
633
|
clearInterval(heartbeat);
|
|
634
|
+
releaseProvisionLock();
|
|
559
635
|
inFlight = null;
|
|
560
636
|
});
|
|
561
637
|
}
|
|
@@ -629,6 +705,11 @@ export function ensureRuntimeProvisioned() {
|
|
|
629
705
|
// live without a reinstall). Best-effort; never blocks startup.
|
|
630
706
|
export async function ensureHarnessPatched() {
|
|
631
707
|
try {
|
|
708
|
+
// Also a boot-time actor on HARNESS_DIR (git checkout/apply below) — skip
|
|
709
|
+
// while another process is actively provisioning it (cloning/resetting/
|
|
710
|
+
// installing) rather than racing on the same checkout.
|
|
711
|
+
if (anotherProcessProvisioning())
|
|
712
|
+
return { ok: false, detail: "provisioning in progress elsewhere" };
|
|
632
713
|
// Version marker of the CURRENT vendored patch: a symbol that exists only
|
|
633
714
|
// in its newest revision (v2 added the daemon.py websocket-proxy
|
|
634
715
|
// neutralization after v1's cdp_urlopen alone proved insufficient — the
|
package/mcp/dist/setup.js
CHANGED
|
@@ -16,12 +16,98 @@ import { sendStateSnapshot } from "./telemetry.js";
|
|
|
16
16
|
// Per-install scoping list lives outside the repo so it survives repo updates.
|
|
17
17
|
const STATE_DIR = process.env.S4L_STATE_DIR || path.join(os.homedir(), ".social-autoposter-mcp");
|
|
18
18
|
const STATE_PATH = path.join(STATE_DIR, "setup-state.json");
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
19
|
+
// Canonical home of config.json (2026-07-13): the STATE DIR, not the package.
|
|
20
|
+
// Config is user data; the package is replaceable app payload. Nothing that
|
|
21
|
+
// replaces the package (updates, re-materialize, stray-checkout heal) can
|
|
22
|
+
// touch the state dir, which ends the recurring operator-Mac config splits.
|
|
23
|
+
export function stateConfigPath() {
|
|
24
|
+
return path.join(STATE_DIR, "config.json");
|
|
25
|
+
}
|
|
26
|
+
// The pipeline reads projects[] from config.json. Resolution order:
|
|
27
|
+
// 1. $S4L_CONFIG_PATH — explicit override (tests / unusual installs)
|
|
28
|
+
// 2. the state-dir config, whenever it exists (canonical since 2026-07-13;
|
|
29
|
+
// ensureConfigInStateDir() migrates legacy installs on boot)
|
|
30
|
+
// 3. legacy $repo/config.json — pre-migration installs and fresh setups
|
|
31
|
+
// (the first boot after setup migrates the file up to the state dir)
|
|
32
|
+
// The result is realpath-resolved: atomic rename-style writers would
|
|
33
|
+
// otherwise REPLACE a symlink with a plain file instead of writing through
|
|
34
|
+
// it (the 2026-07-11/13 operator config splits). realpath of a plain file is
|
|
35
|
+
// itself, so customer installs see no change. Resolved per call, not a
|
|
36
|
+
// load-time const, because a bare .mcpb install materializes the repo after
|
|
37
|
+
// boot and setup must write config.json into THAT repo.
|
|
23
38
|
export function configPath() {
|
|
24
|
-
|
|
39
|
+
const p = process.env.S4L_CONFIG_PATH ||
|
|
40
|
+
(fs.existsSync(stateConfigPath()) ? stateConfigPath() : path.join(repoDir(), "config.json"));
|
|
41
|
+
try {
|
|
42
|
+
return fs.realpathSync(p);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return p; // not created yet: return the intended path unresolved
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// One-time (idempotent) migration to the state-dir home, run at server boot.
|
|
49
|
+
// Cases:
|
|
50
|
+
// - no state config, legacy is a REAL file -> move it up, leave a symlink
|
|
51
|
+
// behind so direct-path readers (shell jq, third-party scripts, older
|
|
52
|
+
// package lanes) keep working through the old location.
|
|
53
|
+
// - no state config, legacy is a symlink -> leave it alone (an operator
|
|
54
|
+
// arrangement points somewhere deliberate; never shadow it).
|
|
55
|
+
// - state config exists, legacy is a REAL file -> a pre-migration lane
|
|
56
|
+
// wrote after the move (or a legacy split): park it as a timestamped
|
|
57
|
+
// .bak next to the package copy and re-link. State wins; the backup
|
|
58
|
+
// preserves whatever diverged for manual inspection.
|
|
59
|
+
// - state config exists, legacy missing/wrong link -> (re)create the link.
|
|
60
|
+
// Best-effort, never throws; the resolver works with or without it.
|
|
61
|
+
export function ensureConfigInStateDir() {
|
|
62
|
+
try {
|
|
63
|
+
if (process.env.S4L_CONFIG_PATH)
|
|
64
|
+
return "skip: S4L_CONFIG_PATH override";
|
|
65
|
+
const state = stateConfigPath();
|
|
66
|
+
const legacy = path.join(repoDir(), "config.json");
|
|
67
|
+
if (state === legacy)
|
|
68
|
+
return "skip: state and legacy resolve identically";
|
|
69
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
70
|
+
let legacyStat = null;
|
|
71
|
+
try {
|
|
72
|
+
legacyStat = fs.lstatSync(legacy);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* absent */
|
|
76
|
+
}
|
|
77
|
+
if (!fs.existsSync(state)) {
|
|
78
|
+
if (legacyStat?.isFile()) {
|
|
79
|
+
fs.renameSync(legacy, state);
|
|
80
|
+
fs.symlinkSync(state, legacy);
|
|
81
|
+
return `migrated ${legacy} -> ${state} (symlink left behind)`;
|
|
82
|
+
}
|
|
83
|
+
return "no state config yet; nothing to migrate";
|
|
84
|
+
}
|
|
85
|
+
if (legacyStat?.isSymbolicLink()) {
|
|
86
|
+
try {
|
|
87
|
+
if (fs.realpathSync(legacy) === fs.realpathSync(state))
|
|
88
|
+
return "ok";
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
/* dangling link: fall through and repoint */
|
|
92
|
+
}
|
|
93
|
+
fs.unlinkSync(legacy);
|
|
94
|
+
fs.symlinkSync(state, legacy);
|
|
95
|
+
return "repointed legacy symlink at state config";
|
|
96
|
+
}
|
|
97
|
+
if (legacyStat?.isFile()) {
|
|
98
|
+
const bak = `${legacy}.pre-statedir-${new Date().toISOString().replace(/[:.]/g, "-")}.bak`;
|
|
99
|
+
fs.renameSync(legacy, bak);
|
|
100
|
+
fs.symlinkSync(state, legacy);
|
|
101
|
+
console.error(`[setup] config divergence: ${legacy} was a real file while the state config exists; ` +
|
|
102
|
+
`parked it at ${bak} and re-linked. State config wins.`);
|
|
103
|
+
return `divergence parked at ${bak}`;
|
|
104
|
+
}
|
|
105
|
+
fs.symlinkSync(state, legacy);
|
|
106
|
+
return "created legacy symlink at state config";
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
return `ensureConfigInStateDir error: ${e?.message || e}`;
|
|
110
|
+
}
|
|
25
111
|
}
|
|
26
112
|
// Fields the X drafting prompts genuinely consume. Required ones must all be
|
|
27
113
|
// present before a project is "ready"; recommended ones improve draft quality.
|
|
@@ -275,6 +361,57 @@ export function applySetup(input) {
|
|
|
275
361
|
fields_removed,
|
|
276
362
|
};
|
|
277
363
|
}
|
|
364
|
+
// Persist the connected Reddit username into config accounts.reddit.username
|
|
365
|
+
// (the field account_resolver.resolve('reddit') reads), following the same
|
|
366
|
+
// never-clobber rule connect_x uses for the X handle: only write when the
|
|
367
|
+
// configured value is empty or the template placeholder. This is the MCP
|
|
368
|
+
// server's config-write path (configPath() resolution + backup + snapshot);
|
|
369
|
+
// setup_reddit_auth.py deliberately does NOT write config.json itself.
|
|
370
|
+
const REDDIT_USERNAME_PLACEHOLDERS = new Set(["", "your-reddit-username", "u/your-reddit-username"]);
|
|
371
|
+
export function recordRedditAccount(username) {
|
|
372
|
+
const clean = (username || "").trim().replace(/^u\//i, "").replace(/^@/, "");
|
|
373
|
+
if (!clean)
|
|
374
|
+
return { written: false, detail: "empty username" };
|
|
375
|
+
let cfg;
|
|
376
|
+
try {
|
|
377
|
+
cfg = readConfig();
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
return { written: false, detail: `config unreadable: ${e?.message || e}` };
|
|
381
|
+
}
|
|
382
|
+
const accounts = (cfg.accounts ??= {});
|
|
383
|
+
if (typeof accounts !== "object" || accounts === null) {
|
|
384
|
+
return { written: false, detail: "config accounts is not an object" };
|
|
385
|
+
}
|
|
386
|
+
const reddit = (accounts.reddit ??= {});
|
|
387
|
+
if (typeof reddit !== "object" || reddit === null) {
|
|
388
|
+
return { written: false, detail: "config accounts.reddit is not an object" };
|
|
389
|
+
}
|
|
390
|
+
const cur = String(reddit.username ?? "").trim();
|
|
391
|
+
if (!REDDIT_USERNAME_PLACEHOLDERS.has(cur.toLowerCase())) {
|
|
392
|
+
return {
|
|
393
|
+
written: false,
|
|
394
|
+
detail: `accounts.reddit.username already set (${cur}); not overwriting`,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
reddit.username = clean;
|
|
398
|
+
if (!reddit.login_method)
|
|
399
|
+
reddit.login_method = "browser";
|
|
400
|
+
try {
|
|
401
|
+
const cfgPath = configPath();
|
|
402
|
+
if (fs.existsSync(cfgPath)) {
|
|
403
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
404
|
+
fs.copyFileSync(cfgPath, `${cfgPath}.bak-${stamp}`);
|
|
405
|
+
}
|
|
406
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
407
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
408
|
+
void sendStateSnapshot("config_write");
|
|
409
|
+
return { written: true, detail: `accounts.reddit.username = ${clean}` };
|
|
410
|
+
}
|
|
411
|
+
catch (e) {
|
|
412
|
+
return { written: false, detail: `config write failed: ${e?.message || e}` };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
278
415
|
// ---------------------------------------------------------------------------
|
|
279
416
|
// Personal-brand PERSONA project (2026-06-26).
|
|
280
417
|
//
|
package/mcp/dist/telemetry.js
CHANGED
|
@@ -229,10 +229,20 @@ export async function sendHeartbeat(reason) {
|
|
|
229
229
|
// (heavy, privacy), identity.json (already rides the X-Installation header),
|
|
230
230
|
// browser profiles/cookies, locks, panel-endpoint.json.
|
|
231
231
|
// Mirrors setup.ts configPath(). Re-derived here (not imported) so setup.ts can
|
|
232
|
-
// import sendStateSnapshot from this module without a cycle.
|
|
232
|
+
// import sendStateSnapshot from this module without a cycle. Same resolution
|
|
233
|
+
// order: explicit override, then the state-dir config (canonical since
|
|
234
|
+
// 2026-07-13) whenever it exists, then the legacy in-repo path; realpath so a
|
|
235
|
+
// symlinked legacy location reads the same bytes writers target.
|
|
233
236
|
function snapshotConfigPath() {
|
|
234
|
-
|
|
235
|
-
|
|
237
|
+
const stateCfg = path.join(snapshotStateDir(), "config.json");
|
|
238
|
+
const p = process.env.S4L_CONFIG_PATH ||
|
|
239
|
+
(fs.existsSync(stateCfg) ? stateCfg : path.join(repoDir(), "config.json"));
|
|
240
|
+
try {
|
|
241
|
+
return fs.realpathSync(p);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return p;
|
|
245
|
+
}
|
|
236
246
|
}
|
|
237
247
|
// Mirrors index.ts s4lStateDir().
|
|
238
248
|
function snapshotStateDir() {
|
package/mcp/dist/version.json
CHANGED