social-autoposter 1.6.140 → 1.6.142

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mcp/dist/index.js CHANGED
@@ -208,42 +208,41 @@ const server = new McpServer({
208
208
  "can run them hands-free on autopilot.\n\n" +
209
209
  "ONBOARDING IS A TERMINAL GOAL. When the user asks to set up, install, configure, or onboard " +
210
210
  "social-autoposter, keep taking the next safe action until the owned runtime is ready, a " +
211
- "project is fully configured with seeded search topics, X is connected with its real handle, " +
212
- "the two draft-autopilot scheduled tasks have been created via queue_setup, AND the scheduled " +
213
- "cycle has produced a draft card (poll the `dashboard` pending-draft count) verifying the ACTUAL pipeline end to end " +
214
- "without posting (see SCHEDULE THE AUTOPILOT below). The owned runtime (private Python + Chromium) provisions " +
211
+ "project is fully configured with seeded search topics, X is connected with its real handle, AND " +
212
+ "the two draft-autopilot scheduled tasks have been created via queue_setup and are firing " +
213
+ "(see SCHEDULE THE AUTOPILOT below). The owned runtime (private Python + Chromium) provisions " +
215
214
  "ITSELF automatically on boot — you do NOT install it. Just poll runtime action:'status' until it " +
216
215
  "reports ready; only call runtime action:'install' or action:'doctor' to TROUBLESHOOT if status " +
217
216
  "shows the boot provision failed or stalled. Do not ask whether to inspect " +
218
217
  "status, repair a failed runtime, choose an auto-detected browser profile, connect " +
219
- "X, scan the profile, research the product website, save conservative inferred fields, seed " +
220
- "topics, retry a recoverable failure, or run draft-only verification. Do those things. The " +
218
+ "X, scan the profile, research the product website, save conservative inferred fields, or seed " +
219
+ "topics. Do those things. The " +
221
220
  "explicit setup request authorizes runtime repair and importing ONLY x.com/" +
222
221
  "twitter.com session cookies into the managed browser: warn that macOS keychain prompts may " +
223
222
  "appear, then proceed without a separate yes/no turn. Ask at most one bundled question, only " +
224
223
  "when no product can be identified from config, context, the X profile, or public research, " +
225
- "or pause for an unavoidable interactive login or OS prompt. Never auto-POST during setup, but " +
226
- "DO schedule the draft autopilot and verify it produced a draft card as the final step (it only " +
227
- "queues drafts for approval, it never posts on its own). When setup reaches done (" +
228
- "runtime ready, a project configured with seeded topics, X connected, the two scheduled tasks " +
229
- "created, and one real cycle has produced a draft card), call the `dashboard` tool so the user " +
224
+ "or pause for an unavoidable interactive login or OS prompt. Never auto-POST during setup. " +
225
+ "DO schedule the draft autopilot as the final step (once running it only queues drafts for " +
226
+ "approval, it never posts on its own). When setup reaches done (" +
227
+ "runtime ready, a project configured with seeded topics, X connected, and the two scheduled tasks " +
228
+ "created and firing), call the `dashboard` tool so the user " +
230
229
  "sees the finished setup rendered, then give " +
231
230
  "the completion summary.\n\n" +
232
- "SCHEDULE + VERIFY THE AUTOPILOT (final onboarding step). There is ONE drafting pipeline: a launchd " +
231
+ "SCHEDULE THE AUTOPILOT (final onboarding step). There is ONE drafting pipeline: a launchd " +
233
232
  "job this server installs runs the real cycle in draft-only mode, its Phase-2b Claude turn is " +
234
233
  "serviced by a scheduled task that drains a local job queue, and the result is merged into the " +
235
234
  "menu-bar approval cards. Drafting is the queue + worker path, fired automatically by a launchd " +
236
- "kicker this server installs (every few minutes). Steps, in order: " +
235
+ "kicker this server installs (every few minutes) it runs on its own once the tasks exist; " +
236
+ "onboarding does NOT need to wait for or verify a draft. Steps, in order: " +
237
237
  "(1) call `queue_setup` — it pre-writes the worker task prompts and returns two task specs " +
238
238
  "(saps-phase1-query, saps-phase2b-draft); " +
239
239
  "(2) for EACH spec, call the HOST tool `create_scheduled_task` with its taskId, cronExpression, and " +
240
240
  "prompt VERBATIM (if it says the task already exists, that's fine — move on, do not recreate); " +
241
- "(3) the launchd kicker + scheduled tasks now run the pipeline on their own — poll the `dashboard` tool every ~30s " +
242
- "for up to ~5 minutes until the pending-draft count rises that card came through the real " +
243
- "pipeline and verifies it end to end. Do NOT create the old 'social-autoposter-autopilot' task " +
244
- "it is deprecated. Do NOT ask the user to review the drafts or pause for input mid-onboarding; " +
245
- "keep going until the card appears, then summarize. If the host exposes no create_scheduled_task " +
246
- "tool, tell the user the autopilot couldn't be scheduled here.\n\n" +
241
+ "(3) the launchd kicker + scheduled tasks now run the pipeline on their own — call the `dashboard` " +
242
+ "tool to confirm the schedule is firing (schedule_state 'ok'), then summarize. Do NOT create the " +
243
+ "old 'social-autoposter-autopilot' task it is deprecated. Do NOT ask the user to review drafts " +
244
+ "or pause for input mid-onboarding; once the tasks are created and firing, setup is done. If the " +
245
+ "host exposes no create_scheduled_task tool, tell the user the autopilot couldn't be scheduled here.\n\n" +
247
246
  "BE PROACTIVE ABOUT MARKETING MOMENTS. Whenever the user ships, finishes, merges, or releases " +
248
247
  "something worth talking about in this session (a new feature, a launch, a long-awaited fix, a " +
249
248
  "milestone), don't wait to be asked: point it out to the user — the draft autopilot runs " +
@@ -1430,7 +1429,7 @@ tool("project_config", {
1430
1429
  (x.connected ? "" : " X is not connected yet either — detect_x_sources, warn about keychain prompts, then run connect_x with confirm:true without a separate permission turn.")
1431
1430
  : projects.every((p) => p.ready)
1432
1431
  ? (x.connected
1433
- ? "All configured projects are ready and X is connected. SCHEDULE + VERIFY THE AUTOPILOT: (1) call queue_setup and create each returned task with create_scheduled_task (prompt verbatim; 'already exists' is fine); (2) the autopilot then runs on its own (launchd kicker + queue worker) poll the `dashboard` tool for ~5 min until the pending-draft count rises — that card came through the real pipeline. Do NOT pause to ask the user to review drafts. Then call `dashboard` so the user sees the finished setup."
1432
+ ? "All configured projects are ready and X is connected. SCHEDULE THE AUTOPILOT: (1) call queue_setup and create each returned task with create_scheduled_task (prompt verbatim; 'already exists' is fine); (2) the autopilot then runs on its own (launchd kicker + queue worker). Call the `dashboard` tool to confirm the schedule is firing (schedule_state 'ok') — that is the terminal step; do NOT wait for or verify a draft card. Do NOT pause to ask the user to review drafts."
1434
1433
  : "All configured projects are ready, but X is NOT connected — posting needs a logged-in " +
1435
1434
  "x.com session. Detect sources and run project_config action:'connect_x', confirm:true; do not ask whether to proceed.")
1436
1435
  : "Some projects are missing required fields (see each project's missing_required). Derive them from config, context, profile_scan, and website research, then call project_config again. Ask only if a required field is genuinely unknowable." +
@@ -1561,8 +1560,9 @@ tool("project_config", {
1561
1560
  ? `Project '${result.project}' is fully configured.${seedNote} Next: if X is not connected, ` +
1562
1561
  `detect sources, warn about keychain prompts, and call project_config with ` +
1563
1562
  `action:'connect_x', confirm:true immediately. Once X is connected, schedule the autopilot ` +
1564
- `(queue_setup + create_scheduled_task per task); the autopilot then drafts on its own poll the ` +
1565
- `dashboard until a draft card appears — that verifies the real pipeline without posting.`
1563
+ `(queue_setup + create_scheduled_task per task); the autopilot then drafts on its own. Call the ` +
1564
+ `dashboard to confirm the schedule is firing (schedule_state 'ok') — that is the final step, ` +
1565
+ `no need to wait for or verify a draft card.`
1566
1566
  : `Saved what you provided for '${result.project}'. Still need: ${result.missing_required.join(", ")}. ` +
1567
1567
  `First derive those fields from existing context, profile_scan, and website research, then ` +
1568
1568
  `call project_config again with name='${result.project}'. Ask only if a required field is genuinely unknowable.`) +
@@ -2616,62 +2616,29 @@ async function ensureMemorySnapshotInstalled() {
2616
2616
  }
2617
2617
  }
2618
2618
  // Is the draft schedule registered AND running for the LIVE account?
2619
- // 'ok' — worker tasks present+enabled and FIRING (lastRunAt within
2620
- // SCHEDULE_FIRING_MS) — the host is actively running them.
2619
+ // 'ok' — worker tasks present+enabled and FIRING (host actively running).
2621
2620
  // 'disabled' — present but a worker task is disabled.
2622
2621
  // 'missing' — not firing anywhere (orphaned / not registered for the live
2623
2622
  // account) -> dashboard offers "Set up draft schedule".
2624
- // The live account is identified by the registry the host is actually FIRING
2625
- // (freshest lastRunAt only the active account's scheduler advances it), NOT a
2626
- // session id (which churns on every Claude restart and mis-read "missing" while
2627
- // the tasks were firing). Mirrors s4l_menubar.py::_schedule_state.
2628
- const SCHEDULE_FIRING_MS = 420_000; // 7 min; tolerates throttle + restart gaps
2629
- function scheduleState() {
2630
- let newestMs = null;
2631
- let newestEnabled = false;
2632
- let anyPresent = false;
2633
- let anyEnabled = false;
2623
+ // The algorithm (live-account detection by freshest lastRunAt, firing window,
2624
+ // etc.) lives in ONE place: scripts/schedule_state.py. The Python menu bar imports
2625
+ // that module in-process; we shell out to it here. Keeping a single implementation
2626
+ // is the whole point — the two surfaces can no longer drift. The script is
2627
+ // stdlib-only and resolvePython() falls back to system python3, so this works even
2628
+ // before the owned runtime is provisioned. Any failure -> "missing" (safe: a
2629
+ // schedule we can't read is treated as not-firing, which only ever surfaces the
2630
+ // re-arm affordance, never a false "ok").
2631
+ async function scheduleState() {
2634
2632
  try {
2635
- const base = path.join(os.homedir(), "Library", "Application Support", "Claude", "claude-code-sessions");
2636
- for (const ws of fs.readdirSync(base)) {
2637
- for (const inner of fs.readdirSync(path.join(base, ws))) {
2638
- const reg = path.join(base, ws, inner, "scheduled-tasks.json");
2639
- let d;
2640
- try {
2641
- d = JSON.parse(fs.readFileSync(reg, "utf-8"));
2642
- }
2643
- catch {
2644
- continue;
2645
- }
2646
- const byId = {};
2647
- for (const t of d.scheduledTasks || [])
2648
- byId[t.id] = t;
2649
- const recs = QUEUE_WORKERS.map((s) => byId[s.taskId]);
2650
- if (recs.some((r) => !r))
2651
- continue;
2652
- anyPresent = true;
2653
- const enabled = recs.every((r) => r.enabled);
2654
- anyEnabled = anyEnabled || enabled;
2655
- const epochs = recs
2656
- .map((r) => Date.parse(r.lastRunAt || ""))
2657
- .filter((n) => !Number.isNaN(n));
2658
- const e = epochs.length ? Math.max(...epochs) : null;
2659
- if (e !== null && (newestMs === null || e > newestMs)) {
2660
- newestMs = e;
2661
- newestEnabled = enabled;
2662
- }
2663
- }
2664
- }
2633
+ const res = await runPython("scripts/schedule_state.py", [], { timeoutMs: 15_000 });
2634
+ const state = JSON.parse(res.stdout.trim()).state;
2635
+ if (state === "ok" || state === "disabled")
2636
+ return state;
2637
+ return "missing";
2665
2638
  }
2666
2639
  catch {
2667
- /* base missing -> treat as missing */
2640
+ return "missing";
2668
2641
  }
2669
- if (newestMs !== null && Date.now() - newestMs <= SCHEDULE_FIRING_MS) {
2670
- return newestEnabled ? "ok" : "disabled";
2671
- }
2672
- if (anyPresent && !anyEnabled)
2673
- return "disabled";
2674
- return "missing";
2675
2642
  }
2676
2643
  // Assemble everything the panel needs in one shot (projects + X + autopilot +
2677
2644
  // version). Resilient: any probe that throws degrades to a safe default rather
@@ -2705,6 +2672,17 @@ async function buildSnapshot() {
2705
2672
  // milestone-ledger count, which disagreed with this whenever a milestone was
2706
2673
  // added late or never recorded, causing the 7/8-vs-"set up" flip-flop).
2707
2674
  const setupComplete = rtReady && projects.some((p) => p.ready) && !!x.connected;
2675
+ // Read the REAL schedule state at all times (during setup AND while running) —
2676
+ // no setup_complete fallback that lied "ok". Source of truth: schedule_state.py.
2677
+ // The re-arm WARNING stays gated on setup_complete below (we don't nag the user
2678
+ // mid-onboarding); only the value here is always honest.
2679
+ const schedule_state = await scheduleState();
2680
+ // The two draft-autopilot tasks are registered AND firing -> the terminal
2681
+ // onboarding milestone is satisfied (this replaced the removed draft_verified,
2682
+ // which depended on the deleted run_draft_cycle tool and could never complete).
2683
+ if (schedule_state === "ok") {
2684
+ completeOnboardingMilestone("tasks_scheduled");
2685
+ }
2708
2686
  const snap = {
2709
2687
  projects,
2710
2688
  projects_total: projects.length,
@@ -2718,9 +2696,9 @@ async function buildSnapshot() {
2718
2696
  // autopilot_stalled is the true "drafts aren't being produced" signal.
2719
2697
  autopilot_stalled: setupComplete && autopilotStalled(),
2720
2698
  // The ACTUAL schedule state for the CURRENT account ('missing'/'disabled'/
2721
- // 'ok'/'unknown'). Drives the dashboard's "Set up draft schedule" button —
2722
- // read directly from the host registry, not inferred from firing.
2723
- schedule_state: setupComplete ? scheduleState() : "ok",
2699
+ // 'ok'). Always the real registry reading; drives the dashboard's "Set up
2700
+ // draft schedule" button (gated on setup_complete in the consumer, not here).
2701
+ schedule_state,
2724
2702
  auto_update_on: ap.auto_update_on,
2725
2703
  version: ver.installed || VERSION,
2726
2704
  latest_version: ver.latest ?? null,
@@ -18,7 +18,7 @@ export const ONBOARDING_MILESTONES = [
18
18
  "mode_chosen",
19
19
  "project_ready",
20
20
  "topics_seeded",
21
- "draft_verified",
21
+ "tasks_scheduled",
22
22
  ];
23
23
  const ledgerApi = require("../shared/onboarding-ledger.cjs");
24
24
  const doctorApi = require("../shared/doctor.cjs");
@@ -70,7 +70,7 @@ Boolean requesting whether a visible border and background is provided by the ho
70
70
  - omitted: host decides border`)});m({method:u("ui/request-display-mode"),params:m({mode:et.describe("The display mode being requested.")})});var mh=m({mode:et.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),ph=U([u("model"),u("app")]).describe("Tool visibility scope - who can access the tool.");m({resourceUri:d().optional(),visibility:z(ph).optional().describe(`Who can access this tool. Default: ["model", "app"]
71
71
  - "model": Tool visible to and callable by the agent
72
72
  - "app": Tool callable by the app from this server only`),csp:Ie().optional(),permissions:Ie().optional()});m({mimeTypes:z(d()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')});m({method:u("ui/download-file"),params:m({contents:z(U([fl,hl])).describe("Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types.")})});m({method:u("ui/message"),params:m({role:u("user").describe('Message role, currently only "user" is supported.'),content:z(yt).describe("Message content blocks (text, image, etc.).")})});m({method:u("ui/notifications/sandbox-resource-ready"),params:m({html:d().describe("HTML content to load into the inner iframe."),sandbox:d().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:po.optional().describe("CSP configuration from resource metadata."),permissions:fo.optional().describe("Sandbox permissions from resource metadata.")})});var fh=m({method:u("ui/notifications/tool-result"),params:Fn.describe("Standard MCP tool execution result.")}),yl=m({toolInfo:m({id:ht.optional().describe("JSON-RPC id of the tools/call request."),tool:mo.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:Yf.optional().describe("Current color theme preference."),styles:ch.optional().describe("Style configuration for theming the app."),displayMode:et.optional().describe("How the UI is currently displayed."),availableDisplayModes:z(et).optional().describe("Display modes the host supports."),containerDimensions:U([m({height:T().describe("Fixed container height in pixels.")}),m({maxHeight:U([T(),Qe()]).optional().describe("Maximum container height in pixels.")})]).and(U([m({width:T().describe("Fixed container width in pixels.")}),m({maxWidth:U([T(),Qe()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
73
- container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:d().optional().describe("User's language and region preference in BCP 47 format."),timeZone:d().optional().describe("User's timezone in IANA format."),userAgent:d().optional().describe("Host application identifier."),platform:U([u("web"),u("desktop"),u("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:m({touch:M().optional().describe("Whether the device supports touch input."),hover:M().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:m({top:T().describe("Top safe area inset in pixels."),right:T().describe("Right safe area inset in pixels."),bottom:T().describe("Bottom safe area inset in pixels."),left:T().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),hh=m({method:u("ui/notifications/host-context-changed"),params:yl.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:z(yt).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:Z(d(),q().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});m({method:u("ui/initialize"),params:m({appInfo:qn.describe("App identification (name and version)."),appCapabilities:dh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var gh=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:qn.describe("Host application identification and version."),hostCapabilities:lh.describe("Features and capabilities provided by the host."),hostContext:yl.describe("Rich context about the host environment.")}).passthrough(),vh={target:"draft-2020-12"};async function Ho(t,n){let r=t["~standard"];if(r.jsonSchema)return r.jsonSchema[n](vh);if(r.vendor==="zod"){let{z:o}=await Ul(()=>Promise.resolve().then(()=>ip),void 0,import.meta.url);return o.toJSONSchema(t,{io:n})}throw Error(`Schema (vendor: ${r.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function Fo(t,n,r=""){let o=await t["~standard"].validate(n);if(o.issues){let e=o.issues.map(i=>{var s;let a=(s=i.path)==null?void 0:s.map(c=>typeof c=="object"?c.key:c).join(".");return a?`${a}: ${i.message}`:i.message}).join("; ");throw Error(r+e)}return o.value}function _h(t){let n=document.documentElement;n.setAttribute("data-theme",t),n.style.colorScheme=t}function bh(t,n=document.documentElement){for(let[r,o]of Object.entries(t))o!==void 0&&n.style.setProperty(r,o)}function $h(t){if(document.getElementById("__mcp-host-fonts"))return;let n=document.createElement("style");n.id="__mcp-host-fonts",n.textContent=t,document.head.appendChild(n)}const At=class At extends Wf{constructor(r,o={},e={autoResize:!0}){super(e);D(this,"_appInfo");D(this,"_capabilities");D(this,"options");D(this,"_hostCapabilities");D(this,"_hostInfo");D(this,"_hostContext");D(this,"_registeredTools",{});D(this,"_initializedSent",!1);D(this,"eventSchemas",{toolinput:ih,toolinputpartial:oh,toolresult:fh,toolcancelled:ah,hostcontextchanged:hh});D(this,"_everHadListener",new Set);D(this,"_toolHandlersInitialized",!1);D(this,"_onteardown");D(this,"_oncalltool");D(this,"_onlisttools");D(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=e,e.allowUnsafeEval||X({jitless:!0}),this.setRequestHandler(Hn,i=>(console.log("Received ping:",i.params),{})),this.setEventHandler("hostcontextchanged",void 0)}_assertInitialized(r){var e;if(this._initializedSent)return;let o=`[ext-apps] App.${r}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if((e=this.options)!=null&&e.strict)throw Error(o);console.warn(`${o}. This will throw in a future release.`)}_assertHandlerTiming(r){var e;if(!At.ONE_SHOT_EVENTS.has(r)||this._everHadListener.has(r)||(this._everHadListener.add(r),!this._initializedSent))return;let o=`[ext-apps] "${String(r)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if((e=this.options)!=null&&e.strict)throw Error(o);console.warn(o)}setEventHandler(r,o){o&&this._assertHandlerTiming(r),super.setEventHandler(r,o)}addEventListener(r,o){this._assertHandlerTiming(r),super.addEventListener(r,o)}onEventDispatch(r,o){r==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...o})}registerCapabilities(r){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._capabilities=Bf(this._capabilities,r)}registerTool(r,o,e){if(this._registeredTools[r])throw Error(`Tool ${r} is already registered`);let i=this,a=()=>{var p;i._initializedSent&&((p=i._capabilities.tools)!=null&&p.listChanged)&&i.sendToolListChanged()},s=o.inputSchema!==void 0,c={title:o.title,description:o.description,inputSchema:o.inputSchema,outputSchema:o.outputSchema,annotations:o.annotations,_meta:o._meta,enabled:!0,enable(){this.enabled=!0,a()},disable(){this.enabled=!1,a()},update(p){Object.assign(this,p),a()},remove(){i._registeredTools[r]===c&&(delete i._registeredTools[r],a())},handler:async(p,b)=>{if(!c.enabled)throw Error(`Tool ${r} is disabled`);let g;if(s){let y=c.inputSchema,w=y?await Fo(y,p??{},`Invalid input for tool ${r}: `):p??{};g=await e(w,b)}else g=await e(b);return c.outputSchema&&!g.isError&&(g.structuredContent=await Fo(c.outputSchema,g.structuredContent,`Invalid output for tool ${r}: `)),g}};return this._registeredTools[r]=c,!this._capabilities.tools&&!this.transport&&this.registerCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),a(),c}ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(r,o)=>{let e=this._registeredTools[r.name];if(!e)throw Error(`Tool ${r.name} not found`);return e.handler(r.arguments,o)},this.onlisttools=async(r,o)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([e,i])=>i.enabled).map(async([e,i])=>{let a={name:e,title:i.title,description:i.description,inputSchema:i.inputSchema?await Ho(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await Ho(i.outputSchema,"output")),i.annotations&&(a.annotations=i.annotations),i._meta&&(a._meta=i._meta),a}))}))}async sendToolListChanged(r={}){this._assertInitialized("sendToolListChanged"),await this.notification({method:"notifications/tools/list_changed",params:r})}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}get ontoolinput(){return this.getEventHandler("toolinput")}set ontoolinput(r){this.setEventHandler("toolinput",r)}get ontoolinputpartial(){return this.getEventHandler("toolinputpartial")}set ontoolinputpartial(r){this.setEventHandler("toolinputpartial",r)}get ontoolresult(){return this.getEventHandler("toolresult")}set ontoolresult(r){this.setEventHandler("toolresult",r)}get ontoolcancelled(){return this.getEventHandler("toolcancelled")}set ontoolcancelled(r){this.setEventHandler("toolcancelled",r)}get onhostcontextchanged(){return this.getEventHandler("hostcontextchanged")}set onhostcontextchanged(r){this.setEventHandler("hostcontextchanged",r)}get onteardown(){return this._onteardown}set onteardown(r){this.warnIfRequestHandlerReplaced("onteardown",this._onteardown,r),this._onteardown=r,this.replaceRequestHandler(uh,(o,e)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(o.params,e)})}get oncalltool(){return this._oncalltool}set oncalltool(r){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,r),this._oncalltool=r,this.replaceRequestHandler(vl,(o,e)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(o.params,e)})}get onlisttools(){return this._onlisttools}set onlisttools(r){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,r),this._onlisttools=r,this.replaceRequestHandler(gl,(o,e)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(o.params,e)})}assertCapabilityForMethod(r){var o;switch(r){case"sampling/createMessage":if(!((o=this._hostCapabilities)!=null&&o.sampling))throw Error(`Host does not support sampling (required for ${r})`);break}}assertRequestHandlerCapability(r){switch(r){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${r})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${r} registered`)}}assertNotificationCapability(r){}assertTaskCapability(r){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(r){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(r,o){if(this._assertInitialized("callServerTool"),typeof r=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${r}"). Did you mean: callServerTool({ name: "${r}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:r},Fn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},pl,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},ml,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let e=r.tools?$l:bl;return await this.request({method:"sampling/createMessage",params:r},e,o)}sendMessage(r,o){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:r},rh,o)}sendLog(r){return this.notification({method:"notifications/message",params:r})}updateModelContext(r,o){return this._assertInitialized("updateModelContext"),this.request({method:"ui/update-model-context",params:r},Gi,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},th,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},nh,o)}requestTeardown(r={}){return this.notification({method:"ui/notifications/request-teardown",params:r})}requestDisplayMode(r,o){return this._assertInitialized("requestDisplayMode"),this.request({method:"ui/request-display-mode",params:r},mh,o)}sendSizeChanged(r){return this.notification({method:"ui/notifications/size-changed",params:r})}setupSizeChangedNotifications(){let r=!1,o=0,e=0,i=()=>{r||(r=!0,requestAnimationFrame(()=>{r=!1;let s=document.documentElement,c=s.style.height;s.style.height="max-content";let p=Math.ceil(s.getBoundingClientRect().height);s.style.height=c;let b=Math.ceil(window.innerWidth);(b!==o||p!==e)&&(o=b,e=p,this.sendSizeChanged({width:b,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(r=new Qf(window.parent,window.parent),o){var e;if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1,await super.connect(r);try{let i=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:Kf}},gh,o);if(i===void 0)throw Error(`Server sent invalid initialize result: ${i}`);this._hostCapabilities=i.hostCapabilities,this._hostInfo=i.hostInfo,this._hostContext=i.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,(e=this.options)!=null&&e.autoResize&&this.setupSizeChangedNotifications()}catch(i){throw this.close(),i}}};D(At,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let er=At;function Jo(t){const n=t&&t.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(t&&t.content||[]).find(o=>o&&o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{}}return{}}class yh{constructor(){D(this,"onhostcontextchanged");D(this,"onerror");D(this,"ontoolresult")}async connect(){var n,r;try{const[o,e]=await Promise.all([this.callServerTool({name:"project_config",arguments:{status:!0}}),this.callServerTool({name:"runtime",arguments:{action:"status"}})]),i=Jo(o),a=Jo(e),s=Array.isArray(i.projects)?i.projects:[],c={projects:s,projects_total:s.length,projects_ready:s.filter(p=>p&&p.ready).length,x_connected:!!i.x_connected,x_state:i.x_state||"",x_handle:i.x_handle??null,version:i.mcp_version||"",latest_version:i.latest_version??null,update_available:!!i.update_available,runtime_ready:typeof a.runtime_ready=="boolean"?a.runtime_ready:!0,runtime_provisioning:!!a.provisioning,onboarding:a.onboarding||i.onboarding};(n=this.ontoolresult)==null||n.call(this,{structuredContent:{snapshot:JSON.stringify(c)}})}catch(o){(r=this.onerror)==null||r.call(this,o)}}getHostContext(){}async callServerTool(n){const r=await fetch(`/tool/${encodeURIComponent(n.name)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n.arguments??{})});if(!r.ok){let o=`HTTP ${r.status}`;try{o=await r.text()||o}catch{}return{isError:!0,content:[{type:"text",text:o}]}}return r.json()}async sendMessage(){return{isError:!0}}}function kh(){return globalThis.__SAPS_BRIDGE__==="http"?new yh:new er({name:"S4L Panel",version:"1.0.0"})}function kl(t){const n=t.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(t.content||[]).find(o=>o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{_raw:r.text}}return{}}const V=t=>document.getElementById(t),Sl=V("ver"),Be=V("btn-setup"),Rt=V("btn-schedule"),It=V("stats-grid"),Jn=V("stats-toggle"),Sh=V("log"),wh=V("install-card"),Se=V("setup-summary"),wl=V("onboarding-details"),Ih=V("onboarding-steps"),Vn=V("onboarding-blocker"),Vo=V("onboarding-count"),zh=V("onboarding-bar-fill"),xh=V("live-card"),jh=V("stats-card"),Bo=V("install-steps"),Oe=V("install-err"),he=V("btn-install"),ho=V("btn-live"),go=V("btn-live-stop"),Wo=V("btn-live-front"),Pe=V("live-status"),Zt=V("live-img");let B=null,Bn=!1,zt=!1,Wn=!1,Ue=!1,Je=!1;function Q(t){Sh.textContent=t}function Nh(t){switch(t){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function vo(t){if(!t||!Array.isArray(t.steps)){Bo.innerHTML="";return}Bo.innerHTML=t.steps.map(n=>{const r=n.detail&&n.status!=="pending"?` <span class="detail">${n.status==="error"?n.detail:""}</span>`:"";return`<li class="${n.status}"><span class="glyph">${Nh(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),t.error?(Oe.textContent=t.error,Oe.hidden=!1):Oe.hidden=!0}const Th={environment_checked:"Environment checked",runtime_ready:"Runtime ready",x_connected:"X connected",profile_scanned:"Profile scanned",project_ready:"Project ready",topics_seeded:"Topics seeded",draft_verified:"Draft cycle verified"};function Oh(t){switch(t){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function Il(){wl.hidden=!Ue,Se.setAttribute("aria-expanded",String(Ue)),Se.classList.toggle("expanded",Ue)}function Ph(t){if(!t||!Array.isArray(t.milestones)){Se.hidden=!0,wl.hidden=!0;return}Se.hidden=!1;const n=t.milestones.length,r=t.milestones.filter(e=>e.status==="complete").length,o=!!t.current_blocker&&!t.complete;Se.classList.toggle("complete",t.complete),Se.classList.toggle("blocked",o),Vo.hidden=t.complete,Vo.textContent=o?`${r}/${n} · needs you`:zt?`${r}/${n} · setting up…`:`${r}/${n}`,zh.style.width=n>0?`${Math.round(r/n*100)}%`:"0%",Ih.innerHTML=t.milestones.map(e=>{const i=Th[e.id]||e.id,a=e.attempts>1?` <span class="detail">${e.attempts} attempts</span>`:"";return`<li class="${e.status}"><span class="glyph">${Oh(e.status)}</span><span>${i}${a}</span></li>`}).join(""),t.current_blocker?(Vn.textContent=`Current blocker: ${t.current_blocker.message}`,Vn.hidden=!1,Ue=!0):Vn.hidden=!0,Il()}function Ct(){if(!B)return;Ph(B.onboarding),Sl.innerHTML=B.update_available&&B.latest_version?`v${B.version} · <button id="btn-update" class="update-btn">Update to ${B.latest_version}</button>`:`v${B.version}`;const t=!B.runtime_ready;wh.hidden=!t;const n=B.projects_ready>0,r=!t&&n&&B.x_connected;Be.hidden=r,Be.disabled=!1,Be.classList.toggle("primary",!r);const o=r&&(B.schedule_state==="missing"||B.schedule_state==="disabled");Rt.hidden=!o,Rt.classList.toggle("primary",o),xh.hidden=!r,jh.hidden=!r}function Me(t){B={...B||{},...t},Ct()}function Uh(t){const n=Array.isArray(t.projects)?t.projects:[];return{projects:n,projects_total:n.length,projects_ready:n.filter(r=>r.ready).length,x_connected:!!t.x_connected,x_state:t.x_state||"",x_handle:t.x_handle??null,version:t.mcp_version||(B==null?void 0:B.version)||"",latest_version:t.latest_version??null,update_available:!!t.update_available,onboarding:t.onboarding}}const be=kh();function zl(t){var n,r,o;t.theme&&_h(t.theme),(n=t.styles)!=null&&n.variables&&bh(t.styles.variables),(o=(r=t.styles)==null?void 0:r.css)!=null&&o.fonts&&$h(t.styles.css.fonts)}be.onhostcontextchanged=zl;be.onerror=t=>console.error(t);be.ontoolresult=t=>{const n=kl(t);n&&typeof n.projects_total=="number"&&(Me(n),n.runtime_ready?xl():n.runtime_provisioning&&bo())};async function fe(t,n={}){const r=await be.callServerTool({name:t,arguments:n});return kl(r)}async function _o(){Q("Refreshing…");try{const[t,n]=await Promise.all([fe("project_config",{status:!0}),fe("runtime",{action:"status"}).catch(()=>({}))]);Me({...Uh(t),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},onboarding:n.onboarding||t.onboarding||(B==null?void 0:B.onboarding)}),B&&!B.runtime_ready&&n.provisioning&&bo(),Q(""),xl()}catch(t){Q("Refresh failed: "+((t==null?void 0:t.message)||t))}}async function bo(){if(!Bn){Bn=!0,he.disabled=!0,he.textContent="Installing…";try{for(;;){const t=await fe("runtime",{action:"status"}).catch(()=>({}));if(vo(t.progress??null),t.onboarding&&Me({onboarding:t.onboarding}),t.runtime_ready){Me({runtime_ready:!0}),Q("Runtime installed; you're ready to set up."),_o();return}const n=t.progress??null;if(n&&n.done&&!n.ok){he.disabled=!1,he.textContent="Retry install",Q("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{Bn=!1}}}async function Eh(){var r;if(zt)return;zt=!0,Ct();const t=Date.now(),n=1200*1e3;try{for(;;){const o=await fe("runtime",{action:"status"}).catch(()=>({}));o.progress&&vo(o.progress);const e={};if(typeof o.runtime_ready=="boolean"&&(e.runtime_ready=o.runtime_ready),o.onboarding&&(e.onboarding=o.onboarding),Object.keys(e).length&&Me(e),(r=o.onboarding)!=null&&r.complete){await _o(),Q("Setup complete.");break}if(Date.now()-t>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{zt=!1,Ct()}}async function xl(){try{const t=await fe("get_stats",{days:7}),n=Array.isArray(t.projects)?t.projects[0]:null,r=n==null?void 0:n.posts;if(!r){It.innerHTML='<div class="muted">No stats yet.</div>';return}const o=[["Posts",r.total??0],["Active",r.active??0],["Views",r.views_period_total??r.views??0],["Replies",r.comments_period_total??r.comments??0],["Clicks",r.post_clicks_period_total??0]];It.innerHTML=o.map(([e,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${e}</div></div>`).join("")}catch(t){It.innerHTML=`<div class="muted">Stats unavailable: ${(t==null?void 0:t.message)||t}</div>`}}function $o(t,n,r){const o=t.textContent;t.disabled=!0,t.textContent=n,r().finally(()=>{t.disabled=!1,t.textContent=o,Ct()})}Be.addEventListener("click",()=>$o(Be,"Starting…",async()=>{Q("Asking Claude to run setup…");try{const t=await be.sendMessage({role:"user",content:[{type:"text",text:"Set up S4L end to end now. Inspect and repair the runtime, auto-detect and connect my X session, scan my profile, discover and research my product, then infer and save a complete project with seeded search topics. Keep going without asking me to approve each safe setup step. Ask only if I must interactively sign in or no product can be identified. Keep every reply to me extremely concise: a few short sentences at most, no step-by-step narration or long status walls. If you must ask me something (e.g. the product URL), make it one short question."}]});t!=null&&t.isError?Q("The host rejected the setup request — type “set up S4L” in the chat instead."):(Q("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),Eh())}catch(t){Q("Couldn’t start setup: "+((t==null?void 0:t.message)||t))}}));Rt.addEventListener("click",()=>$o(Rt,"Setting up…",async()=>{Q("Asking Claude to schedule the draft tasks for this account…");try{const t=await be.sendMessage({role:"user",content:[{type:"text",text:'Set up the social-autoposter draft autopilot schedule for this Claude account. If queue_setup is available, call it; then for EACH of saps-phase1-query and saps-phase2b-draft call the host tool create_scheduled_task with taskId, cronExpression "* * * * *", and the prompt — read it from ~/.claude/scheduled-tasks/<taskId>/SKILL.md (already on disk). Do NOT redo my X connection or project setup. Keep replies to me very short.'}]});t!=null&&t.isError?Q("The host rejected it — type “set up the draft schedule” in the chat instead."):Q("Scheduling is running in the chat. The draft tasks will register for this account.")}catch(t){Q("Couldn’t start scheduling: "+((t==null?void 0:t.message)||t))}}));Se.addEventListener("click",()=>{Ue=!Ue,Il()});Jn.addEventListener("click",()=>{Je=!Je,It.hidden=!Je,Jn.setAttribute("aria-expanded",String(Je)),Jn.classList.toggle("expanded",Je)});Sl.addEventListener("click",t=>{const n=t.target;n&&n.id==="btn-update"&&Dh()});async function Dh(){if(Wn)return;Wn=!0;const t=document.getElementById("btn-update");t&&(t.disabled=!0,t.textContent="Updating…"),Q("Installing the latest release… this can take a minute.");try{const n=await fe("runtime",{action:"update"});n.ok?(Q(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),t&&(t.textContent="Update installed — restart to apply")):(Q("Update failed (exit "+(n.exit_code??"?")+"). Try `npx social-autoposter@latest update` in a terminal."),t&&(t.disabled=!1,t.textContent="Retry update"))}catch(n){Q("Update failed: "+((n==null?void 0:n.message)||n)),t&&(t.disabled=!1,t.textContent="Retry update")}finally{Wn=!1}}he.addEventListener("click",async()=>{Oe.hidden=!0,he.disabled=!0,he.textContent="Starting…",Q("Installing the runtime — this is a one-time download (~150MB+).");try{const t=await fe("runtime",{action:"install"});if(t.runtime_ready){Me({runtime_ready:!0}),_o();return}vo(t.progress??null),bo()}catch(t){he.disabled=!1,he.textContent="Retry install",Oe.textContent="Couldn't start install: "+((t==null?void 0:t.message)||t),Oe.hidden=!1}});let xt=null,Kn=!1;async function Ko(){if(!Kn){Kn=!0;try{const t=await fe("show_browser_to_user",{action:"frame"});if(!t.ok){Pe.textContent=t.message||"No active browser session.",yo(!1);return}t.frame&&(Zt.src=t.frame,Zt.hidden=!1);const n=t.title||t.url||(t.port?"port "+t.port:"");Pe.textContent=t.frame?"Watching"+(n?": "+n:""):"Connecting…"}catch(t){Pe.textContent="Live view error: "+((t==null?void 0:t.message)||t)}finally{Kn=!1}}}function Rh(){ho.hidden=!0,go.hidden=!1,Pe.textContent="Attaching to the browser…",Ko(),xt=setInterval(Ko,450)}function yo(t=!0){xt!=null&&(clearInterval(xt),xt=null),ho.hidden=!1,go.hidden=!0,Zt.hidden=!0,Zt.removeAttribute("src"),t&&fe("show_browser_to_user",{action:"stop"}).catch(()=>{})}ho.addEventListener("click",Rh);go.addEventListener("click",()=>{yo(!0),Pe.textContent=""});Wo.addEventListener("click",()=>$o(Wo,"Bringing…",async()=>{yo(!0);const t=await fe("show_browser_to_user",{action:"front"});Pe.textContent=t!=null&&t.ok?"Brought the browser to the front.":(t==null?void 0:t.message)||"Couldn't bring the browser to the front."}));be.connect().then(()=>{const t=be.getHostContext();t&&zl(t)});</script>
73
+ container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:d().optional().describe("User's language and region preference in BCP 47 format."),timeZone:d().optional().describe("User's timezone in IANA format."),userAgent:d().optional().describe("Host application identifier."),platform:U([u("web"),u("desktop"),u("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:m({touch:M().optional().describe("Whether the device supports touch input."),hover:M().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:m({top:T().describe("Top safe area inset in pixels."),right:T().describe("Right safe area inset in pixels."),bottom:T().describe("Bottom safe area inset in pixels."),left:T().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),hh=m({method:u("ui/notifications/host-context-changed"),params:yl.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:z(yt).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:Z(d(),q().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});m({method:u("ui/initialize"),params:m({appInfo:qn.describe("App identification (name and version)."),appCapabilities:dh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var gh=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:qn.describe("Host application identification and version."),hostCapabilities:lh.describe("Features and capabilities provided by the host."),hostContext:yl.describe("Rich context about the host environment.")}).passthrough(),vh={target:"draft-2020-12"};async function Ho(t,n){let r=t["~standard"];if(r.jsonSchema)return r.jsonSchema[n](vh);if(r.vendor==="zod"){let{z:o}=await Ul(()=>Promise.resolve().then(()=>ip),void 0,import.meta.url);return o.toJSONSchema(t,{io:n})}throw Error(`Schema (vendor: ${r.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function Fo(t,n,r=""){let o=await t["~standard"].validate(n);if(o.issues){let e=o.issues.map(i=>{var s;let a=(s=i.path)==null?void 0:s.map(c=>typeof c=="object"?c.key:c).join(".");return a?`${a}: ${i.message}`:i.message}).join("; ");throw Error(r+e)}return o.value}function _h(t){let n=document.documentElement;n.setAttribute("data-theme",t),n.style.colorScheme=t}function bh(t,n=document.documentElement){for(let[r,o]of Object.entries(t))o!==void 0&&n.style.setProperty(r,o)}function $h(t){if(document.getElementById("__mcp-host-fonts"))return;let n=document.createElement("style");n.id="__mcp-host-fonts",n.textContent=t,document.head.appendChild(n)}const At=class At extends Wf{constructor(r,o={},e={autoResize:!0}){super(e);D(this,"_appInfo");D(this,"_capabilities");D(this,"options");D(this,"_hostCapabilities");D(this,"_hostInfo");D(this,"_hostContext");D(this,"_registeredTools",{});D(this,"_initializedSent",!1);D(this,"eventSchemas",{toolinput:ih,toolinputpartial:oh,toolresult:fh,toolcancelled:ah,hostcontextchanged:hh});D(this,"_everHadListener",new Set);D(this,"_toolHandlersInitialized",!1);D(this,"_onteardown");D(this,"_oncalltool");D(this,"_onlisttools");D(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=e,e.allowUnsafeEval||X({jitless:!0}),this.setRequestHandler(Hn,i=>(console.log("Received ping:",i.params),{})),this.setEventHandler("hostcontextchanged",void 0)}_assertInitialized(r){var e;if(this._initializedSent)return;let o=`[ext-apps] App.${r}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if((e=this.options)!=null&&e.strict)throw Error(o);console.warn(`${o}. This will throw in a future release.`)}_assertHandlerTiming(r){var e;if(!At.ONE_SHOT_EVENTS.has(r)||this._everHadListener.has(r)||(this._everHadListener.add(r),!this._initializedSent))return;let o=`[ext-apps] "${String(r)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if((e=this.options)!=null&&e.strict)throw Error(o);console.warn(o)}setEventHandler(r,o){o&&this._assertHandlerTiming(r),super.setEventHandler(r,o)}addEventListener(r,o){this._assertHandlerTiming(r),super.addEventListener(r,o)}onEventDispatch(r,o){r==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...o})}registerCapabilities(r){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._capabilities=Bf(this._capabilities,r)}registerTool(r,o,e){if(this._registeredTools[r])throw Error(`Tool ${r} is already registered`);let i=this,a=()=>{var p;i._initializedSent&&((p=i._capabilities.tools)!=null&&p.listChanged)&&i.sendToolListChanged()},s=o.inputSchema!==void 0,c={title:o.title,description:o.description,inputSchema:o.inputSchema,outputSchema:o.outputSchema,annotations:o.annotations,_meta:o._meta,enabled:!0,enable(){this.enabled=!0,a()},disable(){this.enabled=!1,a()},update(p){Object.assign(this,p),a()},remove(){i._registeredTools[r]===c&&(delete i._registeredTools[r],a())},handler:async(p,b)=>{if(!c.enabled)throw Error(`Tool ${r} is disabled`);let g;if(s){let y=c.inputSchema,w=y?await Fo(y,p??{},`Invalid input for tool ${r}: `):p??{};g=await e(w,b)}else g=await e(b);return c.outputSchema&&!g.isError&&(g.structuredContent=await Fo(c.outputSchema,g.structuredContent,`Invalid output for tool ${r}: `)),g}};return this._registeredTools[r]=c,!this._capabilities.tools&&!this.transport&&this.registerCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),a(),c}ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(r,o)=>{let e=this._registeredTools[r.name];if(!e)throw Error(`Tool ${r.name} not found`);return e.handler(r.arguments,o)},this.onlisttools=async(r,o)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([e,i])=>i.enabled).map(async([e,i])=>{let a={name:e,title:i.title,description:i.description,inputSchema:i.inputSchema?await Ho(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await Ho(i.outputSchema,"output")),i.annotations&&(a.annotations=i.annotations),i._meta&&(a._meta=i._meta),a}))}))}async sendToolListChanged(r={}){this._assertInitialized("sendToolListChanged"),await this.notification({method:"notifications/tools/list_changed",params:r})}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}get ontoolinput(){return this.getEventHandler("toolinput")}set ontoolinput(r){this.setEventHandler("toolinput",r)}get ontoolinputpartial(){return this.getEventHandler("toolinputpartial")}set ontoolinputpartial(r){this.setEventHandler("toolinputpartial",r)}get ontoolresult(){return this.getEventHandler("toolresult")}set ontoolresult(r){this.setEventHandler("toolresult",r)}get ontoolcancelled(){return this.getEventHandler("toolcancelled")}set ontoolcancelled(r){this.setEventHandler("toolcancelled",r)}get onhostcontextchanged(){return this.getEventHandler("hostcontextchanged")}set onhostcontextchanged(r){this.setEventHandler("hostcontextchanged",r)}get onteardown(){return this._onteardown}set onteardown(r){this.warnIfRequestHandlerReplaced("onteardown",this._onteardown,r),this._onteardown=r,this.replaceRequestHandler(uh,(o,e)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(o.params,e)})}get oncalltool(){return this._oncalltool}set oncalltool(r){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,r),this._oncalltool=r,this.replaceRequestHandler(vl,(o,e)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(o.params,e)})}get onlisttools(){return this._onlisttools}set onlisttools(r){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,r),this._onlisttools=r,this.replaceRequestHandler(gl,(o,e)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(o.params,e)})}assertCapabilityForMethod(r){var o;switch(r){case"sampling/createMessage":if(!((o=this._hostCapabilities)!=null&&o.sampling))throw Error(`Host does not support sampling (required for ${r})`);break}}assertRequestHandlerCapability(r){switch(r){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${r})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${r} registered`)}}assertNotificationCapability(r){}assertTaskCapability(r){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(r){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(r,o){if(this._assertInitialized("callServerTool"),typeof r=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${r}"). Did you mean: callServerTool({ name: "${r}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:r},Fn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},pl,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},ml,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let e=r.tools?$l:bl;return await this.request({method:"sampling/createMessage",params:r},e,o)}sendMessage(r,o){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:r},rh,o)}sendLog(r){return this.notification({method:"notifications/message",params:r})}updateModelContext(r,o){return this._assertInitialized("updateModelContext"),this.request({method:"ui/update-model-context",params:r},Gi,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},th,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},nh,o)}requestTeardown(r={}){return this.notification({method:"ui/notifications/request-teardown",params:r})}requestDisplayMode(r,o){return this._assertInitialized("requestDisplayMode"),this.request({method:"ui/request-display-mode",params:r},mh,o)}sendSizeChanged(r){return this.notification({method:"ui/notifications/size-changed",params:r})}setupSizeChangedNotifications(){let r=!1,o=0,e=0,i=()=>{r||(r=!0,requestAnimationFrame(()=>{r=!1;let s=document.documentElement,c=s.style.height;s.style.height="max-content";let p=Math.ceil(s.getBoundingClientRect().height);s.style.height=c;let b=Math.ceil(window.innerWidth);(b!==o||p!==e)&&(o=b,e=p,this.sendSizeChanged({width:b,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(r=new Qf(window.parent,window.parent),o){var e;if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1,await super.connect(r);try{let i=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:Kf}},gh,o);if(i===void 0)throw Error(`Server sent invalid initialize result: ${i}`);this._hostCapabilities=i.hostCapabilities,this._hostInfo=i.hostInfo,this._hostContext=i.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,(e=this.options)!=null&&e.autoResize&&this.setupSizeChangedNotifications()}catch(i){throw this.close(),i}}};D(At,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let er=At;function Jo(t){const n=t&&t.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(t&&t.content||[]).find(o=>o&&o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{}}return{}}class yh{constructor(){D(this,"onhostcontextchanged");D(this,"onerror");D(this,"ontoolresult")}async connect(){var n,r;try{const[o,e]=await Promise.all([this.callServerTool({name:"project_config",arguments:{status:!0}}),this.callServerTool({name:"runtime",arguments:{action:"status"}})]),i=Jo(o),a=Jo(e),s=Array.isArray(i.projects)?i.projects:[],c={projects:s,projects_total:s.length,projects_ready:s.filter(p=>p&&p.ready).length,x_connected:!!i.x_connected,x_state:i.x_state||"",x_handle:i.x_handle??null,version:i.mcp_version||"",latest_version:i.latest_version??null,update_available:!!i.update_available,runtime_ready:typeof a.runtime_ready=="boolean"?a.runtime_ready:!0,runtime_provisioning:!!a.provisioning,onboarding:a.onboarding||i.onboarding};(n=this.ontoolresult)==null||n.call(this,{structuredContent:{snapshot:JSON.stringify(c)}})}catch(o){(r=this.onerror)==null||r.call(this,o)}}getHostContext(){}async callServerTool(n){const r=await fetch(`/tool/${encodeURIComponent(n.name)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n.arguments??{})});if(!r.ok){let o=`HTTP ${r.status}`;try{o=await r.text()||o}catch{}return{isError:!0,content:[{type:"text",text:o}]}}return r.json()}async sendMessage(){return{isError:!0}}}function kh(){return globalThis.__SAPS_BRIDGE__==="http"?new yh:new er({name:"S4L Panel",version:"1.0.0"})}function kl(t){const n=t.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(t.content||[]).find(o=>o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{_raw:r.text}}return{}}const V=t=>document.getElementById(t),Sl=V("ver"),Be=V("btn-setup"),Rt=V("btn-schedule"),It=V("stats-grid"),Jn=V("stats-toggle"),Sh=V("log"),wh=V("install-card"),Se=V("setup-summary"),wl=V("onboarding-details"),Ih=V("onboarding-steps"),Vn=V("onboarding-blocker"),Vo=V("onboarding-count"),zh=V("onboarding-bar-fill"),xh=V("live-card"),jh=V("stats-card"),Bo=V("install-steps"),Oe=V("install-err"),he=V("btn-install"),ho=V("btn-live"),go=V("btn-live-stop"),Wo=V("btn-live-front"),Pe=V("live-status"),Zt=V("live-img");let B=null,Bn=!1,zt=!1,Wn=!1,Ue=!1,Je=!1;function Q(t){Sh.textContent=t}function Nh(t){switch(t){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function vo(t){if(!t||!Array.isArray(t.steps)){Bo.innerHTML="";return}Bo.innerHTML=t.steps.map(n=>{const r=n.detail&&n.status!=="pending"?` <span class="detail">${n.status==="error"?n.detail:""}</span>`:"";return`<li class="${n.status}"><span class="glyph">${Nh(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),t.error?(Oe.textContent=t.error,Oe.hidden=!1):Oe.hidden=!0}const Th={environment_checked:"Environment checked",runtime_ready:"Runtime ready",x_connected:"X connected",profile_scanned:"Profile scanned",project_ready:"Project ready",topics_seeded:"Topics seeded",tasks_scheduled:"Autopilot scheduled"};function Oh(t){switch(t){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function Il(){wl.hidden=!Ue,Se.setAttribute("aria-expanded",String(Ue)),Se.classList.toggle("expanded",Ue)}function Ph(t){if(!t||!Array.isArray(t.milestones)){Se.hidden=!0,wl.hidden=!0;return}Se.hidden=!1;const n=t.milestones.length,r=t.milestones.filter(e=>e.status==="complete").length,o=!!t.current_blocker&&!t.complete;Se.classList.toggle("complete",t.complete),Se.classList.toggle("blocked",o),Vo.hidden=t.complete,Vo.textContent=o?`${r}/${n} · needs you`:zt?`${r}/${n} · setting up…`:`${r}/${n}`,zh.style.width=n>0?`${Math.round(r/n*100)}%`:"0%",Ih.innerHTML=t.milestones.map(e=>{const i=Th[e.id]||e.id,a=e.attempts>1?` <span class="detail">${e.attempts} attempts</span>`:"";return`<li class="${e.status}"><span class="glyph">${Oh(e.status)}</span><span>${i}${a}</span></li>`}).join(""),t.current_blocker?(Vn.textContent=`Current blocker: ${t.current_blocker.message}`,Vn.hidden=!1,Ue=!0):Vn.hidden=!0,Il()}function Ct(){if(!B)return;Ph(B.onboarding),Sl.innerHTML=B.update_available&&B.latest_version?`v${B.version} · <button id="btn-update" class="update-btn">Update to ${B.latest_version}</button>`:`v${B.version}`;const t=!B.runtime_ready;wh.hidden=!t;const n=B.projects_ready>0,r=!t&&n&&B.x_connected;Be.hidden=r,Be.disabled=!1,Be.classList.toggle("primary",!r);const o=r&&(B.schedule_state==="missing"||B.schedule_state==="disabled");Rt.hidden=!o,Rt.classList.toggle("primary",o),xh.hidden=!r,jh.hidden=!r}function Me(t){B={...B||{},...t},Ct()}function Uh(t){const n=Array.isArray(t.projects)?t.projects:[];return{projects:n,projects_total:n.length,projects_ready:n.filter(r=>r.ready).length,x_connected:!!t.x_connected,x_state:t.x_state||"",x_handle:t.x_handle??null,version:t.mcp_version||(B==null?void 0:B.version)||"",latest_version:t.latest_version??null,update_available:!!t.update_available,onboarding:t.onboarding}}const be=kh();function zl(t){var n,r,o;t.theme&&_h(t.theme),(n=t.styles)!=null&&n.variables&&bh(t.styles.variables),(o=(r=t.styles)==null?void 0:r.css)!=null&&o.fonts&&$h(t.styles.css.fonts)}be.onhostcontextchanged=zl;be.onerror=t=>console.error(t);be.ontoolresult=t=>{const n=kl(t);n&&typeof n.projects_total=="number"&&(Me(n),n.runtime_ready?xl():n.runtime_provisioning&&bo())};async function fe(t,n={}){const r=await be.callServerTool({name:t,arguments:n});return kl(r)}async function _o(){Q("Refreshing…");try{const[t,n]=await Promise.all([fe("project_config",{status:!0}),fe("runtime",{action:"status"}).catch(()=>({}))]);Me({...Uh(t),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},onboarding:n.onboarding||t.onboarding||(B==null?void 0:B.onboarding)}),B&&!B.runtime_ready&&n.provisioning&&bo(),Q(""),xl()}catch(t){Q("Refresh failed: "+((t==null?void 0:t.message)||t))}}async function bo(){if(!Bn){Bn=!0,he.disabled=!0,he.textContent="Installing…";try{for(;;){const t=await fe("runtime",{action:"status"}).catch(()=>({}));if(vo(t.progress??null),t.onboarding&&Me({onboarding:t.onboarding}),t.runtime_ready){Me({runtime_ready:!0}),Q("Runtime installed; you're ready to set up."),_o();return}const n=t.progress??null;if(n&&n.done&&!n.ok){he.disabled=!1,he.textContent="Retry install",Q("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{Bn=!1}}}async function Eh(){var r;if(zt)return;zt=!0,Ct();const t=Date.now(),n=1200*1e3;try{for(;;){const o=await fe("runtime",{action:"status"}).catch(()=>({}));o.progress&&vo(o.progress);const e={};if(typeof o.runtime_ready=="boolean"&&(e.runtime_ready=o.runtime_ready),o.onboarding&&(e.onboarding=o.onboarding),Object.keys(e).length&&Me(e),(r=o.onboarding)!=null&&r.complete){await _o(),Q("Setup complete.");break}if(Date.now()-t>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{zt=!1,Ct()}}async function xl(){try{const t=await fe("get_stats",{days:7}),n=Array.isArray(t.projects)?t.projects[0]:null,r=n==null?void 0:n.posts;if(!r){It.innerHTML='<div class="muted">No stats yet.</div>';return}const o=[["Posts",r.total??0],["Active",r.active??0],["Views",r.views_period_total??r.views??0],["Replies",r.comments_period_total??r.comments??0],["Clicks",r.post_clicks_period_total??0]];It.innerHTML=o.map(([e,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${e}</div></div>`).join("")}catch(t){It.innerHTML=`<div class="muted">Stats unavailable: ${(t==null?void 0:t.message)||t}</div>`}}function $o(t,n,r){const o=t.textContent;t.disabled=!0,t.textContent=n,r().finally(()=>{t.disabled=!1,t.textContent=o,Ct()})}Be.addEventListener("click",()=>$o(Be,"Starting…",async()=>{Q("Asking Claude to run setup…");try{const t=await be.sendMessage({role:"user",content:[{type:"text",text:"Set up S4L end to end now. Inspect and repair the runtime, auto-detect and connect my X session, scan my profile, discover and research my product, then infer and save a complete project with seeded search topics. Keep going without asking me to approve each safe setup step. Ask only if I must interactively sign in or no product can be identified. Keep every reply to me extremely concise: a few short sentences at most, no step-by-step narration or long status walls. If you must ask me something (e.g. the product URL), make it one short question."}]});t!=null&&t.isError?Q("The host rejected the setup request — type “set up S4L” in the chat instead."):(Q("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),Eh())}catch(t){Q("Couldn’t start setup: "+((t==null?void 0:t.message)||t))}}));Rt.addEventListener("click",()=>$o(Rt,"Setting up…",async()=>{Q("Asking Claude to schedule the draft tasks for this account…");try{const t=await be.sendMessage({role:"user",content:[{type:"text",text:'Set up the social-autoposter draft autopilot schedule for this Claude account. If queue_setup is available, call it; then for EACH of saps-phase1-query and saps-phase2b-draft call the host tool create_scheduled_task with taskId, cronExpression "* * * * *", and the prompt — read it from ~/.claude/scheduled-tasks/<taskId>/SKILL.md (already on disk). Do NOT redo my X connection or project setup. Keep replies to me very short.'}]});t!=null&&t.isError?Q("The host rejected it — type “set up the draft schedule” in the chat instead."):Q("Scheduling is running in the chat. The draft tasks will register for this account.")}catch(t){Q("Couldn’t start scheduling: "+((t==null?void 0:t.message)||t))}}));Se.addEventListener("click",()=>{Ue=!Ue,Il()});Jn.addEventListener("click",()=>{Je=!Je,It.hidden=!Je,Jn.setAttribute("aria-expanded",String(Je)),Jn.classList.toggle("expanded",Je)});Sl.addEventListener("click",t=>{const n=t.target;n&&n.id==="btn-update"&&Dh()});async function Dh(){if(Wn)return;Wn=!0;const t=document.getElementById("btn-update");t&&(t.disabled=!0,t.textContent="Updating…"),Q("Installing the latest release… this can take a minute.");try{const n=await fe("runtime",{action:"update"});n.ok?(Q(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),t&&(t.textContent="Update installed — restart to apply")):(Q("Update failed (exit "+(n.exit_code??"?")+"). Try `npx social-autoposter@latest update` in a terminal."),t&&(t.disabled=!1,t.textContent="Retry update"))}catch(n){Q("Update failed: "+((n==null?void 0:n.message)||n)),t&&(t.disabled=!1,t.textContent="Retry update")}finally{Wn=!1}}he.addEventListener("click",async()=>{Oe.hidden=!0,he.disabled=!0,he.textContent="Starting…",Q("Installing the runtime — this is a one-time download (~150MB+).");try{const t=await fe("runtime",{action:"install"});if(t.runtime_ready){Me({runtime_ready:!0}),_o();return}vo(t.progress??null),bo()}catch(t){he.disabled=!1,he.textContent="Retry install",Oe.textContent="Couldn't start install: "+((t==null?void 0:t.message)||t),Oe.hidden=!1}});let xt=null,Kn=!1;async function Ko(){if(!Kn){Kn=!0;try{const t=await fe("show_browser_to_user",{action:"frame"});if(!t.ok){Pe.textContent=t.message||"No active browser session.",yo(!1);return}t.frame&&(Zt.src=t.frame,Zt.hidden=!1);const n=t.title||t.url||(t.port?"port "+t.port:"");Pe.textContent=t.frame?"Watching"+(n?": "+n:""):"Connecting…"}catch(t){Pe.textContent="Live view error: "+((t==null?void 0:t.message)||t)}finally{Kn=!1}}}function Rh(){ho.hidden=!0,go.hidden=!1,Pe.textContent="Attaching to the browser…",Ko(),xt=setInterval(Ko,450)}function yo(t=!0){xt!=null&&(clearInterval(xt),xt=null),ho.hidden=!1,go.hidden=!0,Zt.hidden=!0,Zt.removeAttribute("src"),t&&fe("show_browser_to_user",{action:"stop"}).catch(()=>{})}ho.addEventListener("click",Rh);go.addEventListener("click",()=>{yo(!0),Pe.textContent=""});Wo.addEventListener("click",()=>$o(Wo,"Bringing…",async()=>{yo(!0);const t=await fe("show_browser_to_user",{action:"front"});Pe.textContent=t!=null&&t.ok?"Brought the browser to the front.":(t==null?void 0:t.message)||"Couldn't bring the browser to the front."}));be.connect().then(()=>{const t=be.getHostContext();t&&zl(t)});</script>
74
74
  <style rel="stylesheet" crossorigin>:root{--bg: var(--background, #ffffff);--fg: var(--foreground, #111111);--muted: var(--muted-foreground, #6b6b6b);--card: var(--card, #f5f5f5);--border: var(--border, #e2e2e2);--btn-fg: var(--primary-foreground, #ffffff);--btn-bg: var(--primary, #111111);--btn-border: var(--border, #c4c4c4)}@media(prefers-color-scheme:dark){:root{--bg: var(--background, #1a1a1a);--fg: var(--foreground, #f2f2f2);--muted: var(--muted-foreground, #9a9a9a);--card: var(--card, #262626);--border: var(--border, #3a3a3a);--btn-fg: var(--primary-foreground, #111111);--btn-bg: var(--primary, #f2f2f2);--btn-border: var(--border, #555555)}}*{box-sizing:border-box}html,body{margin:0;padding:0;background:var(--bg);color:var(--fg);font-family:var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);font-size:14px;line-height:1.4}.wrap{max-width:640px;margin:0 auto;padding:14px;display:flex;flex-direction:column;gap:10px}.head{display:flex;align-items:center;justify-content:space-between;gap:8px}.head-left{display:inline-flex;align-items:baseline;gap:8px;min-width:0}.title{font-size:16px;font-weight:650;letter-spacing:-.01em}.ver{font-size:12px;color:var(--muted);display:inline-flex;align-items:center;gap:6px}.ver button.update-btn{padding:3px 10px;font-size:12px;font-weight:600;border-radius:7px;background:var(--btn-bg);color:var(--btn-fg);border:1px solid var(--btn-bg)}.setup-summary{display:inline-flex;align-items:center;gap:6px;flex:none;padding:5px 10px;font-size:12px;font-weight:550;color:var(--muted);background:var(--bg);border:1px solid var(--border);border-radius:8px}.setup-summary[hidden]{display:none}.setup-summary.complete{color:var(--fg)}.setup-summary-label{font-weight:600}.setup-summary-count{color:var(--muted)}.setup-summary-count[hidden]{display:none}.setup-summary.blocked .setup-summary-count{color:var(--fg);font-weight:600}.chevron{width:0;height:0;flex:none;border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid currentColor;transition:transform .15s ease}.expanded .chevron{transform:rotate(180deg)}button{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:1px solid var(--btn-border);background:var(--card);color:var(--fg);border-radius:8px;padding:7px 12px;font-size:13px;font-weight:550;line-height:1.2;white-space:nowrap;cursor:pointer;transition:opacity .12s ease}button:hover:not(:disabled){opacity:.82}button:disabled{opacity:.45;cursor:default}button.primary{background:var(--btn-bg);color:var(--btn-fg);border-color:var(--btn-bg)}button.ghost{background:var(--bg)}button[hidden]{display:none}.actions{display:flex;flex-wrap:wrap;gap:8px}.section{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:8px}.section[hidden]{display:none}.section-row{display:flex;align-items:center;justify-content:space-between;gap:8px 12px;width:100%;flex-wrap:wrap}.section-label{font-size:13px;font-weight:600;color:var(--fg)}.section-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}button.section-row-toggle{background:none;border:none;padding:0;border-radius:0;font:inherit;color:inherit}button.section-row-toggle:hover:not(:disabled){opacity:1}.section-row-toggle .chevron{color:var(--muted)}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(88px,1fr));gap:8px}.stats-grid[hidden]{display:none}.stat{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:8px 10px}.stat .n{font-size:17px;font-weight:650}.stat .l{font-size:11px;color:var(--muted);margin-top:1px}.config-desc{font-size:12px;color:var(--muted)}.config-desc[hidden]{display:none}.config-editor{width:100%;min-height:260px;resize:vertical;background:var(--bg);color:var(--fg);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-family:var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);font-size:12px;line-height:1.45;-moz-tab-size:2;tab-size:2;white-space:pre;overflow:auto}.config-editor[hidden]{display:none}.config-status{font-size:11px;color:var(--muted);white-space:pre-wrap;word-break:break-all}.live-status{font-size:11px;color:var(--muted);min-height:14px}.live-img{display:block;width:100%;border:1px solid var(--border);border-radius:6px;background:#000}.live-img[hidden]{display:none}.install{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:12px;display:flex;flex-direction:column;gap:10px}.install[hidden]{display:none}.install-head{font-size:14px;font-weight:650}.install-desc{font-size:12px;color:var(--muted)}.install-actions{display:flex;gap:8px}.install-err{font-size:12px;color:var(--fg);font-weight:600;white-space:pre-wrap}.install-err[hidden]{display:none}.steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}.steps li{display:flex;align-items:baseline;gap:8px;font-size:13px;color:var(--muted)}.steps li .glyph{width:1.1em;flex:none;text-align:center;font-weight:650;color:var(--fg)}.steps li.running,.steps li.done,.steps li.complete,.steps li.in_progress{color:var(--fg)}.steps li.pending{opacity:.55}.steps li.error,.steps li.blocked{color:var(--fg);font-weight:600}.steps li .detail{font-size:11px;color:var(--muted)}.onboarding-details{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:12px;display:flex;flex-direction:column;gap:10px}.onboarding-details[hidden]{display:none}.onboarding-bar{height:4px;border-radius:999px;background:var(--border);overflow:hidden}.onboarding-bar>span{display:block;height:100%;width:0%;background:var(--fg);border-radius:inherit;transition:width .35s ease}.onboarding-steps{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px 12px}.onboarding-blocker{border-top:1px solid var(--border);padding-top:9px;color:var(--fg);font-size:12px;font-weight:600;white-space:pre-wrap}.onboarding-blocker[hidden]{display:none}@media(max-width:520px){.onboarding-steps{grid-template-columns:1fr}}.log{font-size:12px;color:var(--muted);min-height:16px;white-space:pre-wrap}.muted{color:var(--muted)}</style>
75
75
  </head>
76
76
  <body>
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.6.140",
3
- "installedAt": "2026-06-29T18:58:56.530Z"
2
+ "version": "1.6.142",
3
+ "installedAt": "2026-06-29T20:58:06.674Z"
4
4
  }
package/mcp/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "dxt_version": "0.1",
3
3
  "name": "social-autoposter",
4
4
  "display_name": "S4L",
5
- "version": "1.6.140",
5
+ "version": "1.6.142",
6
6
  "description": "Draft, review, approve, and autopilot X/Twitter posts.",
7
7
  "long_description": "The disclaimer above is generic Claude boilerplate. S4L is an open source product developed by Mediar.ai Incorporated, a VC-backed San Francisco-based startup.\n\nTo get started:\n\n1\\. Copy this prompt: **Set me up on S4L end to end**\n\n2\\. Quit fully with CMD+Q, restart Claude, and paste the prompt into a new chat.",
8
8
  "author": {
@@ -139,11 +139,9 @@ AUTOPILOT_STALL_SECONDS = 180
139
139
  # Keep in sync with RUNNING_STALL_SECONDS (scripts/autopilot_stall_watch.py).
140
140
  AUTOPILOT_RUNNING_STALL_SECONDS = 900
141
141
 
142
- # A worker task whose lastRunAt is within this many seconds is "firing" — the host
143
- # scheduler runs them every minute, so a fresh stamp means the live account's
144
- # schedule is active. 7 min tolerates host throttling + a restart gap without
145
- # false "not scheduled". Used by _schedule_state.
146
- FIRING_WINDOW = 420
142
+ # The "firing" window (how fresh lastRunAt must be) lives in the single source of
143
+ # truth, scripts/schedule_state.py (FIRING_WINDOW there). _schedule_state delegates
144
+ # to it, so it is intentionally NOT redefined here.
147
145
 
148
146
 
149
147
  def _glyph(status):
@@ -389,59 +387,20 @@ class S4LMenuBar(rumps.App):
389
387
  self._open_claude()
390
388
 
391
389
  # ---- schedule-state detection ----------------------------------------
392
- # Identify the LIVE account's registry by which one the host is actually
393
- # FIRING (freshest lastRunAt — only the active account's scheduler advances
394
- # it), then read THAT registry's enabled state. This is robust across the
395
- # session-id churn that restarts cause; the old "active session = newest
396
- # config.json allowlist key" heuristic mis-reported "missing" after a restart
397
- # even while the tasks were firing.
398
- @staticmethod
399
- def _iso_to_epoch(s):
400
- if not s:
401
- return None
402
- try:
403
- import calendar
404
- return calendar.timegm(
405
- time.strptime(str(s).strip().rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S")
406
- )
407
- except Exception:
408
- return None
409
-
410
390
  def _schedule_state(self):
411
391
  """Is the draft schedule registered AND running for the live account?
412
- 'ok' worker tasks present+enabled and FIRING (lastRunAt within
413
- FIRING_WINDOW) the host is actively running them.
414
- 'disabled' present but a worker task is disabled.
415
- 'missing' — not firing anywhere (orphaned / not registered for the live
416
- account) -> offer re-arm.
417
- The active account is identified by the freshest-firing registry, NOT a
418
- session id (which churns on restart)."""
419
- newest_epoch, newest_enabled = None, False
420
- any_present, any_enabled = False, False
421
- for f in glob.glob(SCHED_REGISTRY_GLOB):
422
- try:
423
- with open(f) as fh:
424
- d = json.load(fh)
425
- except Exception:
426
- continue
427
- by_id = {t.get("id"): t for t in (d.get("scheduledTasks") or [])}
428
- recs = [by_id.get(tid) for tid in WORKER_TASK_IDS]
429
- if any(r is None for r in recs):
430
- continue
431
- any_present = True
432
- enabled = all(r.get("enabled") for r in recs)
433
- any_enabled = any_enabled or enabled
434
- epochs = [self._iso_to_epoch(r.get("lastRunAt")) for r in recs]
435
- e = max([x for x in epochs if x is not None], default=None)
436
- if e is not None and (newest_epoch is None or e > newest_epoch):
437
- newest_epoch, newest_enabled = e, enabled
438
- # Firing recently => the live account's schedule is active and healthy.
439
- if newest_epoch is not None and (time.time() - newest_epoch) <= FIRING_WINDOW:
440
- return "ok" if newest_enabled else "disabled"
441
- # Not firing anywhere. Registered-but-disabled => disabled; else missing.
442
- if any_present and not any_enabled:
443
- return "disabled"
444
- return "missing"
392
+ Returns 'ok' | 'disabled' | 'missing'. Delegates to the SINGLE source of
393
+ truth, scripts/schedule_state.py (shared with the Node MCP server, which
394
+ shells out to the same script), so the firing-detection algorithm lives in
395
+ exactly one place and the menu bar + dashboard can never drift. The script
396
+ is on sys.path via the SAPS_REPO_DIR/scripts insertion near the top of this
397
+ file. Any failure -> 'missing' (safe: never a false 'ok')."""
398
+ try:
399
+ import schedule_state
400
+ return schedule_state.compute()
401
+ except Exception as e:
402
+ _capture(e, phase="schedule_state")
403
+ return "missing"
445
404
 
446
405
  # ---- autopilot liveness (the false-green fix) -------------------------
447
406
  def _autopilot_stalled(self):
@@ -1054,7 +1013,10 @@ class S4LMenuBar(rumps.App):
1054
1013
  # for this account -> show re-arm. We deliberately do NOT drive the menu off
1055
1014
  # the drain-status latch anymore: it stayed stale after recovery and made a
1056
1015
  # firing, healthy autopilot look "not set up".
1057
- schedule_state = self._schedule_state() if setup_complete else "ok"
1016
+ # Always read the REAL schedule state (no setup-gated "ok" fallback that
1017
+ # lied). The re-arm WARNING still only fires once setup is complete, so we
1018
+ # never nag the user mid-onboarding — only the value is now always honest.
1019
+ schedule_state = self._schedule_state()
1058
1020
  self._schedule_state_cache = schedule_state
1059
1021
  attention = setup_complete and schedule_state in ("missing", "disabled")
1060
1022
  # Drop the stale "drafting" spinner while we need attention so the ⚠ shows.
@@ -38,7 +38,7 @@ MILESTONES = [
38
38
  "mode_chosen",
39
39
  "project_ready",
40
40
  "topics_seeded",
41
- "draft_verified",
41
+ "tasks_scheduled",
42
42
  ]
43
43
 
44
44
  # Mirrors panel.ts MILESTONE_LABELS.
@@ -50,7 +50,7 @@ MILESTONE_LABELS = {
50
50
  "mode_chosen": "Mode chosen",
51
51
  "project_ready": "Project ready",
52
52
  "topics_seeded": "Topics seeded",
53
- "draft_verified": "Draft cycle verified",
53
+ "tasks_scheduled": "Autopilot scheduled",
54
54
  }
55
55
 
56
56
  # Mirrors index.ts TWITTER_AUTOPILOT_LABEL.
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/social-autoposter-mcp",
3
- "version": "1.6.140",
3
+ "version": "1.6.142",
4
4
  "private": true,
5
5
  "description": "Desktop MCP client for social-autoposter (X/Twitter rail): manual draft/review/approve loop, autopilot control, and stats. Thin wrapper over the existing pipeline scripts.",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "social-autoposter",
3
- "version": "1.6.140",
3
+ "version": "1.6.142",
4
4
  "description": "Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Install as a Claude Code agent skill.",
5
5
  "bin": {
6
6
  "social-autoposter": "bin/cli.js"
@@ -1,22 +1,30 @@
1
1
  #!/usr/bin/env bash
2
2
  # reset-test-machine.sh — wipe a social-autoposter install back to factory-fresh.
3
3
  #
4
- # For TEST MACHINES. Removes everything an install scatters so you can re-run a
5
- # clean first-install and reproduce new-user bugs:
6
- # - MCP state dir (owned uv-python venv + materialized repo + runtime.json + setup-state)
7
- # - the Claude Desktop .mcpb extension (install dir + per-extension settings + installations registry entry)
8
- # - the global npm library
9
- # - the MCP registration (claude mcp + Claude Desktop config)
10
- # - packaged Chrome profiles + imported cookies (browser-harness / reddit / linkedin)
11
- # - the browser-harness backend (clone + uv-tool CLI + server.py)
12
- # - (with --deep) the shared toolchain we package: uv binary, uv cache, packaged Chromium
4
+ # For TEST MACHINES. Removes what a social-autoposter install scatters so you can
5
+ # re-run a clean first-install and reproduce new-user bugs.
13
6
  #
14
7
  # DEFAULT IS A DRY RUN. Nothing is deleted until you pass --yes.
15
8
  #
9
+ # Two scopes:
10
+ # (default) PLUGIN RESET — just the plugin: the menu-bar app, the .mcpb/npm
11
+ # plugin itself, the scheduled tasks, the user's state+config
12
+ # (~/.social-autoposter-mcp, including config.json + mode.json), the
13
+ # /tmp scratch, autopilot transcripts, and the social-autoposter MCP
14
+ # registration. PRESERVES the imported X login (browser-harness
15
+ # profile), all shared browser profiles, the browser-harness backend,
16
+ # and the packaged toolchain. Uninstall + forget the plugin without
17
+ # disturbing the rest of the environment.
18
+ # --deep FULL NUKE — the plugin reset above PLUS the shared layer it touches:
19
+ # per-agent Chrome profiles + cookies (reddit/linkedin/twitter), the
20
+ # browser-harness backend, and the packaged toolchain (uv binary, uv
21
+ # cache, Chromium ~1.7G). Other tools that rely on uv/Chromium
22
+ # re-provision afterward. Use to reproduce a true bare-metal box.
23
+ #
16
24
  # Usage:
17
- # scripts/reset-test-machine.sh # dry run: print what WOULD be removed
18
- # scripts/reset-test-machine.sh --yes # remove owned state (safe set)
19
- # scripts/reset-test-machine.sh --yes --deep # also nuke uv + uv cache + ms-playwright (1.7G)
25
+ # scripts/reset-test-machine.sh # dry run: print what the plugin reset WOULD remove
26
+ # scripts/reset-test-machine.sh --yes # plugin reset (light)
27
+ # scripts/reset-test-machine.sh --yes --deep # full nuke incl. shared browser layer + toolchain
20
28
  #
21
29
  set -u
22
30
 
@@ -34,11 +42,20 @@ for a in "$@"; do
34
42
  esac
35
43
  done
36
44
 
45
+ # The plugin reset is the default; --deep widens it to the shared browser layer +
46
+ # toolchain. PLUGIN_ONLY (the inverse of DEEP) gates the shared-layer steps below.
47
+ if [ "$DEEP" -eq 1 ]; then PLUGIN_ONLY=0; else PLUGIN_ONLY=1; fi
48
+
37
49
  if [ "$DRY" -eq 1 ]; then
38
50
  echo "=== DRY RUN — nothing will be deleted. Re-run with --yes to apply. ==="
39
51
  else
40
52
  echo "=== APPLYING — removing social-autoposter install ==="
41
53
  fi
54
+ if [ "$PLUGIN_ONLY" -eq 1 ]; then
55
+ echo "=== SCOPE: plugin reset (app + plugin + tasks + state/config; keeping X login, browser layer, toolchain). Pass --deep for the full nuke. ==="
56
+ else
57
+ echo "=== SCOPE: --deep full nuke (plugin + shared browser profiles + harness backend + toolchain) ==="
58
+ fi
42
59
  echo
43
60
 
44
61
  # ---- helpers ---------------------------------------------------------------
@@ -327,9 +344,17 @@ fi
327
344
  echo
328
345
 
329
346
  # ---- 3. MCP registration ---------------------------------------------------
347
+ # In the default plugin reset, deregister ONLY social-autoposter; the browser-agent
348
+ # MCPs (twitter-harness/reddit-agent/linkedin-agent) are shared infra we preserve
349
+ # (their profiles + backend survive in steps 4/5). --deep deregisters them too.
330
350
  echo "[3] MCP registration (claude CLI + config files)"
351
+ if [ "$PLUGIN_ONLY" -eq 1 ]; then
352
+ MCP_NAMES="social-autoposter"
353
+ else
354
+ MCP_NAMES="social-autoposter twitter-harness reddit-agent linkedin-agent"
355
+ fi
331
356
  if command -v claude >/dev/null 2>&1; then
332
- for name in social-autoposter twitter-harness reddit-agent linkedin-agent; do
357
+ for name in $MCP_NAMES; do
333
358
  echo " run claude mcp remove $name"
334
359
  [ "$DRY" -eq 0 ] && claude mcp remove "$name" >/dev/null 2>&1 || true
335
360
  done
@@ -347,32 +372,49 @@ done
347
372
  echo
348
373
 
349
374
  # ---- 4. packaged Chrome profiles + imported cookies ------------------------
350
- echo "[4] packaged Chrome profiles + imported cookies"
351
- PROF="$HOME_DIR/.claude/browser-profiles"
352
- for d in browser-harness browser-harness-linkedin browser-harness-reddit reddit linkedin twitter; do
353
- rm_path "$PROF/$d" "profile"
354
- done
355
- # cookie mirrors + harness logs/pids
356
- if [ -d "$PROF" ]; then
357
- for f in "$PROF"/*.x-cookies.json "$PROF"/*.chrome.log "$PROF"/*.chrome.pid \
358
- "$PROF"/*.mcp.log "$PROF"/browser-activity.log*; do
359
- [ -e "$f" ] && rm_path "$f" "cookie/log"
375
+ # Skipped in the default plugin reset: the per-agent profiles (reddit/linkedin/
376
+ # twitter) belong to the OTHER agents, and the browser-harness profile holds the
377
+ # imported X login we preserve. Only --deep removes these.
378
+ if [ "$PLUGIN_ONLY" -eq 1 ]; then
379
+ echo "[4] packaged Chrome profiles + imported cookies — skipped (plugin reset: X login + shared profiles preserved; pass --deep to remove)"
380
+ else
381
+ echo "[4] packaged Chrome profiles + imported cookies"
382
+ PROF="$HOME_DIR/.claude/browser-profiles"
383
+ for d in browser-harness browser-harness-linkedin browser-harness-reddit reddit linkedin twitter; do
384
+ rm_path "$PROF/$d" "profile"
360
385
  done
386
+ # cookie mirrors + harness logs/pids
387
+ if [ -d "$PROF" ]; then
388
+ for f in "$PROF"/*.x-cookies.json "$PROF"/*.chrome.log "$PROF"/*.chrome.pid \
389
+ "$PROF"/*.mcp.log "$PROF"/browser-activity.log*; do
390
+ [ -e "$f" ] && rm_path "$f" "cookie/log"
391
+ done
392
+ fi
361
393
  fi
362
394
  echo
363
395
 
364
396
  # ---- 5. browser-harness backend -------------------------------------------
365
- echo "[5] browser-harness backend (clone + uv-tool CLI + server.py)"
366
- rm_path "$HOME_DIR/Developer/browser-harness" "harness-clone"
367
- rm_path "$HOME_DIR/.claude/mcp-servers/browser-harness" "harness-server"
368
- rm_path "$HOME_DIR/.local/bin/browser-harness" "harness-cli"
369
- if command -v uv >/dev/null 2>&1; then
370
- echo " run uv tool uninstall browser-harness"
371
- [ "$DRY" -eq 0 ] && uv tool uninstall browser-harness >/dev/null 2>&1 || true
397
+ # Skipped in the default plugin reset: the harness backend is shared infra
398
+ # (twitter-harness MCP + per-platform agents drive it), not the plugin itself.
399
+ # Only --deep removes it.
400
+ if [ "$PLUGIN_ONLY" -eq 1 ]; then
401
+ echo "[5] browser-harness backend skipped (plugin reset: shared harness backend preserved; pass --deep to remove)"
402
+ else
403
+ echo "[5] browser-harness backend (clone + uv-tool CLI + server.py)"
404
+ rm_path "$HOME_DIR/Developer/browser-harness" "harness-clone"
405
+ rm_path "$HOME_DIR/.claude/mcp-servers/browser-harness" "harness-server"
406
+ rm_path "$HOME_DIR/.local/bin/browser-harness" "harness-cli"
407
+ if command -v uv >/dev/null 2>&1; then
408
+ echo " run uv tool uninstall browser-harness"
409
+ [ "$DRY" -eq 0 ] && uv tool uninstall browser-harness >/dev/null 2>&1 || true
410
+ fi
372
411
  fi
373
412
  echo
374
413
 
375
414
  # ---- 6. DEEP: shared toolchain we package (uv + chromium) ------------------
415
+ if [ "$PLUGIN_ONLY" -eq 1 ]; then
416
+ echo "[6] shared toolchain (uv, uv cache, packaged Chromium) — skipped (plugin reset; pass --deep to remove)"
417
+ else
376
418
  echo "[6] shared toolchain (uv, uv cache, packaged Chromium) — ${DEEP:+}$([ "$DEEP" -eq 1 ] && echo ENABLED || echo 'skipped (pass --deep)')"
377
419
  if [ "$DEEP" -eq 1 ]; then
378
420
  rm_path "$HOME_DIR/Library/Caches/ms-playwright" "chromium"
@@ -383,6 +425,7 @@ if [ "$DEEP" -eq 1 ]; then
383
425
  else
384
426
  echo " (left uv + ~/Library/Caches/ms-playwright + ~/.cache/uv in place)"
385
427
  fi
428
+ fi
386
429
  echo
387
430
 
388
431
  if [ "$DRY" -eq 1 ]; then
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env python3
2
+ """Single source of truth for the draft-autopilot schedule state.
3
+
4
+ Both the Node MCP server (mcp/src/index.ts::scheduleState, via subprocess) and
5
+ the Python menu bar (mcp/menubar/s4l_menubar.py, via in-process import) read the
6
+ schedule state from HERE so the two surfaces can never drift. Previously the same
7
+ ~40-line algorithm was hand-maintained in both languages.
8
+
9
+ The data source is the host's scheduled-task registries on disk:
10
+ ~/Library/Application Support/Claude/claude-code-sessions/*/*/scheduled-tasks.json
11
+ Both worker tasks (saps-phase1-query, saps-phase2b-draft) must be present; the
12
+ LIVE account is the registry whose tasks have the freshest lastRunAt (only the
13
+ active account's scheduler advances it, so this is robust across the session-id
14
+ churn that Claude restarts cause).
15
+
16
+ States:
17
+ 'ok' — both worker tasks present, enabled, and FIRING (lastRunAt within
18
+ FIRING_WINDOW seconds).
19
+ 'disabled' — present but a worker task is disabled.
20
+ 'missing' — not firing anywhere (orphaned / not registered for the live
21
+ account) -> the dashboard offers "Set up draft schedule".
22
+
23
+ stdlib-only on purpose, so the MCP can run it with system python3 before the
24
+ owned runtime is provisioned. Run as a script -> prints {"state": "..."} as JSON.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import glob
29
+ import json
30
+ import os
31
+ import sys
32
+ import time
33
+
34
+ # Keep in sync with QUEUE_WORKERS in mcp/src/index.ts and WORKER_TASK_IDS in
35
+ # mcp/menubar/s4l_menubar.py.
36
+ WORKER_TASK_IDS = ("saps-phase1-query", "saps-phase2b-draft")
37
+
38
+ # A worker task whose lastRunAt is within this many seconds counts as "firing".
39
+ # 7 min tolerates the host's per-task throttle + Claude restart gaps without a
40
+ # false "not scheduled".
41
+ FIRING_WINDOW = 420
42
+
43
+ SCHED_REGISTRY_GLOB = os.path.join(
44
+ os.path.expanduser("~"), "Library", "Application Support", "Claude",
45
+ "claude-code-sessions", "*", "*", "scheduled-tasks.json",
46
+ )
47
+
48
+
49
+ def _iso_to_epoch(s):
50
+ if not s:
51
+ return None
52
+ try:
53
+ import calendar
54
+ return calendar.timegm(
55
+ time.strptime(str(s).strip().rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S")
56
+ )
57
+ except Exception:
58
+ return None
59
+
60
+
61
+ def compute(glob_pattern: str = SCHED_REGISTRY_GLOB) -> str:
62
+ """Return 'ok' | 'disabled' | 'missing' for the live account's draft schedule."""
63
+ newest_epoch, newest_enabled = None, False
64
+ any_present, any_enabled = False, False
65
+ for f in glob.glob(glob_pattern):
66
+ try:
67
+ with open(f) as fh:
68
+ d = json.load(fh)
69
+ except Exception:
70
+ continue
71
+ by_id = {t.get("id"): t for t in (d.get("scheduledTasks") or [])}
72
+ recs = [by_id.get(tid) for tid in WORKER_TASK_IDS]
73
+ if any(r is None for r in recs):
74
+ continue
75
+ any_present = True
76
+ enabled = all(r.get("enabled") for r in recs)
77
+ any_enabled = any_enabled or enabled
78
+ epochs = [_iso_to_epoch(r.get("lastRunAt")) for r in recs]
79
+ e = max([x for x in epochs if x is not None], default=None)
80
+ if e is not None and (newest_epoch is None or e > newest_epoch):
81
+ newest_epoch, newest_enabled = e, enabled
82
+ # Firing recently => the live account's schedule is active and healthy.
83
+ if newest_epoch is not None and (time.time() - newest_epoch) <= FIRING_WINDOW:
84
+ return "ok" if newest_enabled else "disabled"
85
+ # Not firing anywhere. Registered-but-disabled => disabled; else missing.
86
+ if any_present and not any_enabled:
87
+ return "disabled"
88
+ return "missing"
89
+
90
+
91
+ def main() -> int:
92
+ try:
93
+ state = compute()
94
+ except Exception:
95
+ state = "missing"
96
+ print(json.dumps({"state": state}))
97
+ return 0
98
+
99
+
100
+ if __name__ == "__main__":
101
+ sys.exit(main())
@@ -836,32 +836,48 @@ log "Pre-flight: probing for an X access gate (/account/access, Cloudflare)..."
836
836
  _ACCESS_OUT=$(TWITTER_CDP_URL="${TWITTER_CDP_URL:-http://127.0.0.1:9555}" \
837
837
  python3 "$REPO_DIR/scripts/twitter_access_check.py" --session-probe --wait-ms 12000 2>/dev/null)
838
838
  if printf '%s' "$_ACCESS_OUT" | grep -q '"gated": *true'; then
839
- # Write/refresh the backoff marker with an exponential cooldown.
840
- _NEXT_MINS=$(python3 -c 'import json,sys
839
+ # Write/refresh the backoff marker with an exponential cooldown. Python
840
+ # prints "<next_mins> <consecutive> <cooldown_secs> <gate_age_secs>".
841
+ _GATE_FIELDS=$(python3 -c 'import json,sys
841
842
  gf, now = sys.argv[1], int(sys.argv[2])
842
843
  base, cap, factor = 900, 7200, 2
843
844
  try: prev = json.load(open(gf))
844
845
  except Exception: prev = {}
845
846
  cd = prev.get("cooldown_secs")
846
847
  cd = base if not cd else min(int(cd)*factor, cap)
847
- out = {"first_seen": prev.get("first_seen", now), "last_seen": now,
848
- "reason": "access_gated", "consecutive": int(prev.get("consecutive",0))+1,
849
- "cooldown_secs": cd, "cooldown_until": now+cd}
850
- json.dump(out, open(gf,"w"))
851
- print(cd//60)' "$_GATE_FILE" "$_NOW" 2>/dev/null || echo 15)
848
+ fs = int(prev.get("first_seen", now))
849
+ cons = int(prev.get("consecutive", 0)) + 1
850
+ out = {"first_seen": fs, "last_seen": now, "reason": "access_gated",
851
+ "consecutive": cons, "cooldown_secs": cd, "cooldown_until": now+cd}
852
+ json.dump(out, open(gf, "w"))
853
+ print(cd//60, cons, cd, max(0, now-fs))' "$_GATE_FILE" "$_NOW" 2>/dev/null || echo "15 1 900 0")
854
+ read -r _NEXT_MINS _CONS _CD_SECS _AGE_SECS <<< "$_GATE_FIELDS"
852
855
  log " Pre-flight FAILED: X is gating this session (access gate detected)."
853
856
  log " Probe: $(printf '%s' "$_ACCESS_OUT" | tr '\n' ' ' | tr -s ' ' | sed 's/^ *//')"
854
857
  log " X redirected an authenticated route to /account/access or served a Cloudflare verification page. This is usually datacenter-IP trust degradation: the session cookie is still valid but X hides content from it, so a scan would return phantom 'doesn't exist' results."
855
858
  log " Backoff engaged: next access re-probe in ~${_NEXT_MINS}m (intervening 5-min firings skip without touching Cloudflare)."
856
859
  log " Action: open the harness Chrome (CDP :9555) and complete the verification at https://x.com/account/access once, or route the box through a residential/clean IP. The cycle auto-resumes within one cooldown of the gate lifting."
860
+ # Machine-greppable marker (additive; mirrors the stderr-marker convention
861
+ # bin/server.js parses). Pairs with twitter_access_gate:recovered below.
862
+ echo "twitter_access_gate: gated consecutive=${_CONS} age_s=${_AGE_SECS} next_reprobe_s=${_CD_SECS}" >&2
857
863
  echo "twitter_batches: ended $BATCH_ID"
858
864
  release_lock "twitter-browser" 2>/dev/null || true
859
865
  exit 1
860
866
  fi
861
- # Probe came back clean: clear any prior backoff marker and resume normally.
867
+ # Probe came back clean. If a backoff marker exists we were gated: record the
868
+ # recovery (how long the gate lasted, since first_seen) BEFORE deleting it, so
869
+ # the lift event + duration survive in the log even though the marker is gone.
862
870
  if [ -f "$_GATE_FILE" ]; then
871
+ _REC=$(python3 -c 'import json,sys
872
+ try: d = json.load(open(sys.argv[1]))
873
+ except Exception: d = {}
874
+ now = int(sys.argv[2]); fs = int(d.get("first_seen", now)); cons = int(d.get("consecutive", 0))
875
+ dur = max(0, now-fs)
876
+ print(dur, dur//60, cons)' "$_GATE_FILE" "$_NOW" 2>/dev/null || echo "0 0 0")
877
+ read -r _DUR_S _DUR_M _RCONS <<< "$_REC"
863
878
  rm -f "$_GATE_FILE"
864
- log " X access gate lifted; cleared backoff marker and resuming normal cycle."
879
+ echo "twitter_access_gate: recovered_after_s=${_DUR_S} consecutive=${_RCONS}" >&2
880
+ log " X access gate lifted after ~${_DUR_M}m (${_RCONS} consecutive gated probes); cleared backoff marker and resuming normal cycle."
865
881
  fi
866
882
  log " Pre-flight access OK: $(printf '%s' "$_ACCESS_OUT" | tr '\n' ' ' | tr -s ' ' | sed 's/^ *//')"
867
883