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 +52 -74
- package/mcp/dist/onboarding.js +1 -1
- package/mcp/dist/panel.html +1 -1
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_menubar.py +19 -57
- package/mcp/menubar/s4l_state.py +2 -2
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/reset-test-machine.sh +73 -30
- package/scripts/schedule_state.py +101 -0
- package/skill/run-twitter-cycle.sh +25 -9
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
|
|
213
|
-
"
|
|
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
|
|
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
|
|
226
|
-
"DO schedule the draft autopilot
|
|
227
|
-
"
|
|
228
|
-
"runtime ready, a project configured with seeded topics, X connected, the two scheduled tasks " +
|
|
229
|
-
"created
|
|
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
|
|
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)
|
|
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 —
|
|
242
|
-
"
|
|
243
|
-
"
|
|
244
|
-
"
|
|
245
|
-
"
|
|
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
|
|
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
|
|
1565
|
-
`dashboard
|
|
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 (
|
|
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
|
|
2625
|
-
//
|
|
2626
|
-
//
|
|
2627
|
-
// the
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
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
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
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
|
-
|
|
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'
|
|
2722
|
-
//
|
|
2723
|
-
schedule_state
|
|
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,
|
package/mcp/dist/onboarding.js
CHANGED
|
@@ -18,7 +18,7 @@ export const ONBOARDING_MILESTONES = [
|
|
|
18
18
|
"mode_chosen",
|
|
19
19
|
"project_ready",
|
|
20
20
|
"topics_seeded",
|
|
21
|
-
"
|
|
21
|
+
"tasks_scheduled",
|
|
22
22
|
];
|
|
23
23
|
const ledgerApi = require("../shared/onboarding-ledger.cjs");
|
|
24
24
|
const doctorApi = require("../shared/doctor.cjs");
|
package/mcp/dist/panel.html
CHANGED
|
@@ -70,7 +70,7 @@ Boolean requesting whether a visible border and background is provided by the ho
|
|
|
70
70
|
- omitted: host decides border`)});m({method:u("ui/request-display-mode"),params:m({mode: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>
|
package/mcp/dist/version.json
CHANGED
package/mcp/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"dxt_version": "0.1",
|
|
3
3
|
"name": "social-autoposter",
|
|
4
4
|
"display_name": "S4L",
|
|
5
|
-
"version": "1.6.
|
|
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
|
-
#
|
|
143
|
-
#
|
|
144
|
-
#
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
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.
|
package/mcp/menubar/s4l_state.py
CHANGED
|
@@ -38,7 +38,7 @@ MILESTONES = [
|
|
|
38
38
|
"mode_chosen",
|
|
39
39
|
"project_ready",
|
|
40
40
|
"topics_seeded",
|
|
41
|
-
"
|
|
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
|
-
"
|
|
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.
|
|
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,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
|
|
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
|
|
18
|
-
# scripts/reset-test-machine.sh --yes #
|
|
19
|
-
# scripts/reset-test-machine.sh --yes --deep #
|
|
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
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|