social-autoposter 1.6.43 → 1.6.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mcp/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // social-autoposter MCP server (X/Twitter rail).
3
3
  //
4
- // Three tools, nothing more:
5
- // draft_cycle - scan + draft, surface each thread + drafted reply, run an
6
- // elicitation approve/skip per draft, then post the approved ones.
4
+ // Core tools:
5
+ // draft_cycle - scan + draft, return all drafts as a numbered table for the
6
+ // user to review in chat (posts nothing).
7
+ // post_drafts - post the drafts the user chose by number from a batch.
7
8
  // autopilot - one tool, action = enable | disable | status (launchd job).
8
9
  // get_stats - read-only post + engagement stats.
9
10
  //
@@ -168,7 +169,36 @@ function blockedReasonMessage(reason) {
168
169
  "Check skill/logs/twitter-cycle-*.log on this machine for details, then run draft_cycle again.");
169
170
  }
170
171
  }
171
- async function produceDrafts(project) {
172
+ // Turn a raw run-twitter-cycle.sh stdout line into a short, user-facing
173
+ // progress message — or null when the line isn't a milestone worth surfacing.
174
+ // The cycle script logs every phase via `log()` (tee'd to stdout), so we can
175
+ // follow along live instead of going dark for the minutes Phase 2b-prep takes.
176
+ // Keep this list tight: only lines a *user* benefits from seeing, phrased for
177
+ // someone who has no idea what "phase2a" means.
178
+ function cycleProgressMessage(line) {
179
+ const l = line.trim();
180
+ let m;
181
+ if (/=== Twitter Cycle \(batch=/.test(l))
182
+ return "Starting draft cycle…";
183
+ if ((m = /^Selected projects?:\s*(.+)$/.exec(l)))
184
+ return `Selected project: ${m[1]}`;
185
+ if (/phase=phase1\b/.test(l) || /Phase 1: drafting queries/.test(l))
186
+ return "Searching X for fresh threads…";
187
+ if ((m = /Phase 1 complete.*?has (\d+) candidates?/.exec(l)))
188
+ return `Found ${m[1]} candidate thread${m[1] === "1" ? "" : "s"} — ranking them…`;
189
+ if (/phase=phase2a\b/.test(l) || /candidates by virality_score selected/.test(l))
190
+ return "Scoring and ranking candidates…";
191
+ if (/Phase 2b-prep: Claude reading threads and drafting replies/.test(l))
192
+ return "Drafting replies (the long step — this can take a few minutes)…";
193
+ if ((m = /Engagement style assigned:.*?style=(\S+)/.exec(l)))
194
+ return `Drafting in style: ${m[1]}…`;
195
+ if (/DRAFT_ONLY_PLAN=/.test(l))
196
+ return "Drafts ready — assembling the review table…";
197
+ if ((m = /DRAFT_ONLY_BLOCKED=([a-z0-9_]+)/.exec(l)))
198
+ return `Cycle stopped (${m[1]}).`;
199
+ return null;
200
+ }
201
+ async function produceDrafts(project, onProgress) {
172
202
  // Run the real pipeline in DRAFT_ONLY mode: scan -> score -> draft -> link-gen,
173
203
  // then STOP before posting. The script prints `DRAFT_ONLY_PLAN=<path>` and
174
204
  // leaves the plan on disk for us to review + post. SAPS_FORCE_PROJECT scopes
@@ -179,9 +209,24 @@ async function produceDrafts(project) {
179
209
  };
180
210
  if (project)
181
211
  env.SAPS_FORCE_PROJECT = project;
212
+ let step = 0;
182
213
  const res = await run("bash", ["skill/run-twitter-cycle.sh"], {
183
214
  env,
184
215
  timeoutMs: 900_000, // scan+draft can take several minutes
216
+ // Mirror the cycle's own log lines to THIS server's stderr (so they land
217
+ // in the host's mcp-server-social-autoposter.log, which used to show only
218
+ // the JSON-RPC handshake) AND forward milestone lines to the live progress
219
+ // sink so the chat spinner stops looking frozen.
220
+ onLine: (line) => {
221
+ const t = line.replace(/\s+$/, "");
222
+ if (t.trim())
223
+ console.error(`[draft_cycle] ${t}`);
224
+ if (!onProgress)
225
+ return;
226
+ const msg = cycleProgressMessage(t);
227
+ if (msg)
228
+ onProgress(msg, ++step);
229
+ },
185
230
  });
186
231
  // Prefer the explicit marker; fall back to the newest plan file on disk.
187
232
  const marker = /DRAFT_ONLY_PLAN=\/tmp\/twitter_cycle_plan_(.+)\.json/.exec(res.stdout + "\n" + res.stderr);
@@ -204,87 +249,28 @@ async function produceDrafts(project) {
204
249
  res.stderr.split("\n").slice(-12).join("\n"),
205
250
  };
206
251
  }
207
- // One BATCHED elicitation: present every draft at once as a checkbox grid with
208
- // an optional inline edit per draft, submitted in a single round trip. The user
209
- // ticks which to post (all pre-checked, so the common path is just "submit"),
210
- // optionally rewrites any reply, and confirms ONCE — collapsing N popups into 1.
252
+ // Render every draft in a batch as a numbered, human-readable table. This IS the
253
+ // review surface now: the model relays this table to the user and asks which
254
+ // numbers to post / edit, then posts the chosen ones via the `post_drafts` tool.
211
255
  //
212
- // MCP elicitation schemas must be FLAT primitives (no arrays / nested objects),
213
- // so we generate one `post_<n>` boolean + one `edit_<n>` string per draft.
214
- async function reviewDrafts(plan) {
256
+ // We used to gather approvals through MCP elicitation (a checkbox form), but the
257
+ // desktop "Code tab" host doesn't advertise the `elicitation` capability (only
258
+ // `io.modelcontextprotocol/ui`), so the form never rendered and cycles silently
259
+ // posted nothing. Approval is conversational instead — numbers in chat.
260
+ function renderDraftsTable(plan) {
215
261
  const candidates = plan.candidates || [];
216
- if (candidates.length === 0)
217
- return { approved: 0, skipped: 0, edited: 0, aborted: false };
218
- const properties = {};
219
- const rows = [];
220
- candidates.forEach((c, i) => {
262
+ return candidates
263
+ .map((c, i) => {
221
264
  const n = i + 1;
222
265
  const author = c.thread_author ? `@${c.thread_author}` : "(unknown thread)";
223
266
  const style = c.engagement_style ?? "?";
224
267
  const reply = c.reply_text ?? "(empty)";
225
268
  const link = c.link_url ? ` · link: ${c.link_url}` : "";
226
- rows.push(`[${n}] ${author} (style: ${style})${link}\n` +
269
+ return (`[${n}] ${author} (style: ${style})${link}\n` +
227
270
  ` ${reply.replace(/\n/g, "\n ")}\n` +
228
271
  ` thread: ${c.candidate_url ?? "?"}`);
229
- const preview = reply.length > 140 ? `${reply.slice(0, 140)}…` : reply;
230
- properties[`post_${n}`] = {
231
- type: "boolean",
232
- title: `Post [${n}] ${author}`,
233
- description: preview,
234
- default: true,
235
- };
236
- properties[`edit_${n}`] = {
237
- type: "string",
238
- title: `Rewrite [${n}] (optional)`,
239
- description: "Leave blank to post as drafted. Type your own wording to replace this reply before posting.",
240
- };
241
- });
242
- const message = `Review ${candidates.length} drafted ` +
243
- `${candidates.length === 1 ? "reply" : "replies"}. ` +
244
- `Every draft is pre-checked to post — untick the ones you don't want, ` +
245
- `optionally rewrite any, then submit once.\n\n` +
246
- rows.join("\n\n");
247
- let res;
248
- try {
249
- res = await server.server.elicitInput({
250
- message,
251
- requestedSchema: { type: "object", properties, required: [] },
252
- });
253
- }
254
- catch (e) {
255
- // Host doesn't support elicitation (some Claude Desktop builds). Bail out
256
- // rather than silently posting or silently skipping everything.
257
- return { approved: 0, skipped: 0, edited: 0, aborted: true };
258
- }
259
- if (res.action !== "accept") {
260
- // User cancelled/declined the whole review -> post nothing.
261
- candidates.forEach((c) => (c.approved = false));
262
- return { approved: 0, skipped: 0, edited: 0, aborted: res.action === "cancel" };
263
- }
264
- const content = res.content || {};
265
- let approved = 0;
266
- let skipped = 0;
267
- let edited = 0;
268
- candidates.forEach((c, i) => {
269
- const n = i + 1;
270
- // Pre-checked (default:true): treat anything but an explicit false as "post".
271
- const wantPost = content[`post_${n}`] !== false;
272
- const rawEdit = content[`edit_${n}`];
273
- const edit = typeof rawEdit === "string" ? rawEdit.trim() : "";
274
- if (edit) {
275
- c.reply_text = edit; // inline correction replaces the drafted reply
276
- edited++;
277
- }
278
- if (wantPost) {
279
- c.approved = true;
280
- approved++;
281
- }
282
- else {
283
- c.approved = false;
284
- skipped++;
285
- }
286
- });
287
- return { approved, skipped, edited, aborted: false };
272
+ })
273
+ .join("\n\n");
288
274
  }
289
275
  async function postApproved(batchId, plan) {
290
276
  const approved = (plan.candidates || []).filter((c) => c.approved === true);
@@ -532,25 +518,60 @@ server.registerTool("setup", {
532
518
  return textContent(`Setup failed: ${e.message}`);
533
519
  }
534
520
  });
535
- // ---- draft_cycle: the whole manual loop in one tool -----------------------
521
+ // ---- draft_cycle: scan + draft, then hand the batch to the user for review.
522
+ // Posting is a SEPARATE step (post_drafts) so the user picks by number in chat.
523
+ // This host doesn't support elicitation, so there is no in-tool form: the model
524
+ // relays the table and asks which to post / edit, then calls post_drafts.
536
525
  server.registerTool("draft_cycle", {
537
526
  title: "Draft an X reply cycle",
538
- description: "Scan X, draft replies on this machine, then show ALL drafts at once in a single " +
539
- "checkbox form: tick which to post (every draft pre-checked), optionally rewrite any " +
540
- "reply inline, and submit once. Only the ticked ones post. The entire manual loop in " +
541
- "one call: discover -> draft -> review -> post. Nothing posts without your approval.",
527
+ description: "Scan X and draft replies on this machine, then return ALL drafts as a numbered table " +
528
+ "for review. This tool POSTS NOTHING. Show the table to the user and ask which numbers " +
529
+ "to post and which to rewrite, then call `post_drafts` with their decision and the " +
530
+ "returned batch_id. Flow: discover -> draft -> review in chat -> post_drafts.",
542
531
  inputSchema: {
543
532
  project: z
544
533
  .string()
545
534
  .optional()
546
535
  .describe("Which configured project to draft for. Optional when only one project is set up; required when several are."),
547
536
  },
548
- }, async ({ project }) => {
537
+ }, async ({ project }, extra) => {
549
538
  const r = resolveProject(project);
550
539
  if (!r.ok)
551
540
  return textContent(r.message);
552
541
  const proj = r.project;
553
- const drafted = await produceDrafts(proj);
542
+ // Live progress so the chat doesn't sit on a frozen spinner for minutes.
543
+ // Two channels, both best-effort (a sink failure must never fail the cycle):
544
+ // 1. notifications/message — a log line; the host records it (and some
545
+ // clients show it in a log view). Works with no client opt-in.
546
+ // 2. notifications/progress — drives the status text under the running
547
+ // tool. Only valid when the client supplied a progressToken on the
548
+ // request, so it's guarded on that.
549
+ const progressToken = extra?._meta?.progressToken;
550
+ const sendProgress = async (message, step) => {
551
+ try {
552
+ await extra.sendNotification({
553
+ method: "notifications/message",
554
+ params: { level: "info", logger: "draft_cycle", data: message },
555
+ });
556
+ }
557
+ catch {
558
+ /* ignore */
559
+ }
560
+ if (progressToken !== undefined) {
561
+ try {
562
+ await extra.sendNotification({
563
+ method: "notifications/progress",
564
+ params: { progressToken, progress: step, message },
565
+ });
566
+ }
567
+ catch {
568
+ /* ignore */
569
+ }
570
+ }
571
+ };
572
+ const drafted = await produceDrafts(proj, (message, step) => {
573
+ void sendProgress(message, step);
574
+ });
554
575
  if (drafted.blocked || !drafted.batchId) {
555
576
  return textContent(drafted.blocked ?? "No drafts produced.");
556
577
  }
@@ -558,25 +579,107 @@ server.registerTool("draft_cycle", {
558
579
  if (!plan || !(plan.candidates && plan.candidates.length)) {
559
580
  return textContent(`No drafts in batch ${drafted.batchId}.`);
560
581
  }
561
- const review = await reviewDrafts(plan);
562
- writePlan(drafted.batchId, plan);
563
- if (review.aborted && review.approved === 0) {
564
- return jsonContent({
582
+ const count = plan.candidates.length;
583
+ const table = renderDraftsTable(plan);
584
+ const message = `Drafted ${count} ${count === 1 ? "reply" : "replies"} for "${proj}" ` +
585
+ `(batch ${drafted.batchId}). NOTHING has been posted yet.\n\n` +
586
+ `${table}\n\n` +
587
+ `Show this list to the user and ask which to post and which to edit. They can reply ` +
588
+ `however is natural, e.g. "post 1, 3 and 5", "edit 2: <new wording>", "post all", or ` +
589
+ `"skip all". Editing a draft also posts it. Then call the post_drafts tool with ` +
590
+ `batch_id "${drafted.batchId}" and their decision (post: [numbers], edits: [{n, text}], ` +
591
+ `or post_all: true). Do not post anything the user didn't ask for.`;
592
+ return {
593
+ content: [{ type: "text", text: message }],
594
+ structuredContent: {
565
595
  batch_id: drafted.batchId,
566
- drafted: plan.candidates.length,
567
- review_aborted: true,
568
- note: "Review did not complete (host may not support elicitation, or you cancelled). " +
569
- "Nothing was posted.",
596
+ drafted: count,
597
+ status: "awaiting_decision",
598
+ },
599
+ };
600
+ });
601
+ // ---- post_drafts: post the user's chosen drafts from a batch ---------------
602
+ // Second half of the manual loop. The user reviewed the table from draft_cycle
603
+ // and said which numbers to post / edit; this posts exactly those. Editing a
604
+ // draft implies posting it. Indices are 1-based, matching the table.
605
+ server.registerTool("post_drafts", {
606
+ title: "Post chosen drafts",
607
+ description: "Post the drafts the user approved from a draft_cycle batch. Pass the batch_id from " +
608
+ "draft_cycle and the user's decision by NUMBER (1-based, matching the table): `post` is " +
609
+ "the list of draft numbers to post as drafted; `edits` rewrites a draft's text before " +
610
+ "posting it (editing implies posting); `post_all` posts every draft. Only the chosen " +
611
+ "drafts post; anything not listed is left unposted. Call this ONLY after the user has " +
612
+ "told you which drafts they want.",
613
+ inputSchema: {
614
+ batch_id: z.string().describe("The batch_id returned by draft_cycle."),
615
+ post: z
616
+ .array(z.number().int().positive())
617
+ .optional()
618
+ .describe("1-based draft numbers to post as drafted, e.g. [1, 3, 5]."),
619
+ edits: z
620
+ .array(z.object({ n: z.number().int().positive(), text: z.string() }))
621
+ .optional()
622
+ .describe("Rewrites: each {n, text} replaces draft n's wording, then posts it."),
623
+ post_all: z.boolean().optional().describe("Post every draft in the batch."),
624
+ },
625
+ }, async ({ batch_id, post, edits, post_all }) => {
626
+ const plan = readPlan(batch_id);
627
+ if (!plan || !(plan.candidates && plan.candidates.length)) {
628
+ return textContent(`No drafts found for batch ${batch_id}. Run draft_cycle again to produce a fresh batch.`);
629
+ }
630
+ const candidates = plan.candidates;
631
+ const total = candidates.length;
632
+ const warnings = [];
633
+ const inRange = (n) => n >= 1 && n <= total;
634
+ // Apply edits first; an edited draft is always posted.
635
+ const approve = new Set();
636
+ let editedCount = 0;
637
+ (edits || []).forEach((e) => {
638
+ if (!inRange(e.n)) {
639
+ warnings.push(`ignored edit for #${e.n}: out of range (1-${total})`);
640
+ return;
641
+ }
642
+ const text = (e.text ?? "").trim();
643
+ if (!text) {
644
+ warnings.push(`ignored empty edit for #${e.n}`);
645
+ return;
646
+ }
647
+ candidates[e.n - 1].reply_text = text;
648
+ approve.add(e.n);
649
+ editedCount++;
650
+ });
651
+ if (post_all) {
652
+ for (let i = 1; i <= total; i++)
653
+ approve.add(i);
654
+ }
655
+ (post || []).forEach((n) => {
656
+ if (inRange(n))
657
+ approve.add(n);
658
+ else
659
+ warnings.push(`ignored #${n}: out of range (1-${total})`);
660
+ });
661
+ candidates.forEach((c, i) => (c.approved = approve.has(i + 1)));
662
+ writePlan(batch_id, plan);
663
+ if (approve.size === 0) {
664
+ return jsonContent({
665
+ batch_id,
666
+ drafted: total,
667
+ posted: 0,
668
+ skipped: total,
669
+ edited: editedCount,
670
+ note: "No drafts selected to post. Nothing was posted.",
671
+ warnings,
570
672
  });
571
673
  }
572
- const posted = await postApproved(drafted.batchId, plan);
674
+ const result = await postApproved(batch_id, plan);
573
675
  return jsonContent({
574
- batch_id: drafted.batchId,
575
- drafted: plan.candidates.length,
576
- approved: review.approved,
577
- skipped: review.skipped,
578
- edited: review.edited,
579
- posted,
676
+ batch_id,
677
+ drafted: total,
678
+ posted: approve.size,
679
+ skipped: total - approve.size,
680
+ edited: editedCount,
681
+ result,
682
+ warnings,
580
683
  });
581
684
  });
582
685
  // ---- autopilot: one tool, three actions -----------------------------------
@@ -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:Be.describe("The display mode being requested.")})});var qf=m({mode:Be.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),Hf=U([u("model"),u("app")]).describe("Tool visibility scope - who can access the tool.");m({resourceUri:d().optional(),visibility:z(Hf).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:ye().optional(),permissions:ye().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([Wu,Bu])).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(gt).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:to.optional().describe("CSP configuration from resource metadata."),permissions:no.optional().describe("Sandbox permissions from resource metadata.")})});var Ff=m({method:u("ui/notifications/tool-result"),params:Rn.describe("Standard MCP tool execution result.")}),el=m({toolInfo:m({id:lt.optional().describe("JSON-RPC id of the tools/call request."),tool:eo.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:jf.optional().describe("Current color theme preference."),styles:Cf.optional().describe("Style configuration for theming the app."),displayMode:Be.optional().describe("How the UI is currently displayed."),availableDisplayModes:z(Be).optional().describe("Display modes the host supports."),containerDimensions:U([m({height:O().describe("Fixed container height in pixels.")}),m({maxHeight:U([O(),Je()]).optional().describe("Maximum container height in pixels.")})]).and(U([m({width:O().describe("Fixed container width in pixels.")}),m({maxWidth:U([O(),Je()]).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:O().describe("Top safe area inset in pixels."),right:O().describe("Right safe area inset in pixels."),bottom:O().describe("Bottom safe area inset in pixels."),left:O().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),Jf=m({method:u("ui/notifications/host-context-changed"),params:el.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:z(gt).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:R(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:En.describe("App identification (name and version)."),appCapabilities:Mf.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var Vf=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:En.describe("Host application identification and version."),hostCapabilities:Lf.describe("Features and capabilities provided by the host."),hostContext:el.describe("Rich context about the host environment.")}).passthrough(),Wf={target:"draft-2020-12"};async function zo(t,n){let r=t["~standard"];if(r.jsonSchema)return r.jsonSchema[n](Wf);if(r.vendor==="zod"){let{z:o}=await ul(()=>Promise.resolve().then(()=>Em),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 xo(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 Bf(t){let n=document.documentElement;n.setAttribute("data-theme",t),n.style.colorScheme=t}function Kf(t,n=document.documentElement){for(let[r,o]of Object.entries(t))o!==void 0&&n.style.setProperty(r,o)}function Gf(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 Tt=class Tt extends wf{constructor(r,o={},e={autoResize:!0}){super(e);A(this,"_appInfo");A(this,"_capabilities");A(this,"options");A(this,"_hostCapabilities");A(this,"_hostInfo");A(this,"_hostContext");A(this,"_registeredTools",{});A(this,"_initializedSent",!1);A(this,"eventSchemas",{toolinput:Ef,toolinputpartial:Df,toolresult:Ff,toolcancelled:Rf,hostcontextchanged:Jf});A(this,"_everHadListener",new Set);A(this,"_toolHandlersInitialized",!1);A(this,"_onteardown");A(this,"_oncalltool");A(this,"_onlisttools");A(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=e,e.allowUnsafeEval||X({jitless:!0}),this.setRequestHandler(Dn,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(!Tt.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=Sf(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 xo(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 xo(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 zo(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await zo(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(Af,(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(Gu,(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(Ku,(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},Rn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},Vu,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},Ju,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let e=r.tools?Yu:Xu;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},Uf,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},Ai,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},Tf,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},Pf,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},qf,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 xf(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:If}},Vf,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}}};A(Tt,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let qn=Tt;function tl(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 ie=t=>document.getElementById(t),Qf=ie("ver"),Xf=ie("st-proj"),Yf=ie("st-proj-sub"),eh=ie("st-x"),th=ie("st-x-sub"),nh=ie("st-ap"),rh=ie("st-ap-sub"),Hn=ie("btn-setup"),Nt=ie("btn-draft"),Ot=ie("btn-autopilot"),Me=ie("btn-connectx"),jo=ie("btn-refresh"),Zn=ie("stats-grid"),ih=ie("log");let V=null,Le=!1;function Q(t){ih.textContent=t}function nl(){if(!V)return;Qf.innerHTML=V.update_available&&V.latest_version?`v${V.version} · <span class="update">update to ${V.latest_version}</span>`:`v${V.version}`,Xf.textContent=`${V.projects_ready}/${V.projects_total}`,Yf.textContent=V.projects_total===0?"none configured":V.projects.map(r=>r.name+(r.ready?"":" (incomplete)")).join(", "),eh.textContent=V.x_connected?"Connected":"Not connected",th.textContent=V.x_state||"",Me.hidden=V.x_connected,Le||(Me.textContent="Connect X"),nh.textContent=V.autopilot_on?"On":"Off",rh.textContent=V.auto_update_on?"auto-update on":"",Ot.textContent=V.autopilot_on?"Disable autopilot":"Enable autopilot";const t=V.projects_ready>0;Nt.disabled=!t,Ot.disabled=!t;const n=!t;Hn.classList.toggle("primary",n),Nt.classList.toggle("primary",!n)}function Ke(t){V={...V||{},...t},nl()}function oh(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||"",version:t.mcp_version||(V==null?void 0:V.version)||"",latest_version:t.latest_version??null,update_available:!!t.update_available}}const ke=new qn({name:"Social Autoposter Panel",version:"1.0.0"});function rl(t){var n,r,o;t.theme&&Bf(t.theme),(n=t.styles)!=null&&n.variables&&Kf(t.styles.variables),(o=(r=t.styles)==null?void 0:r.css)!=null&&o.fonts&&Gf(t.styles.css.fonts)}ke.onhostcontextchanged=rl;ke.onerror=t=>console.error(t);ke.ontoolresult=t=>{const n=tl(t);n&&typeof n.projects_total=="number"&&Ke(n)};async function Se(t,n={}){const r=await ke.callServerTool({name:t,arguments:n});return tl(r)}async function ah(){Q("Refreshing…");try{const[t,n]=await Promise.all([Se("setup",{status:!0}),Se("autopilot",{action:"status"})]);Ke({...oh(t),autopilot_on:!!n.loaded,auto_update_on:!!n.auto_update_loaded}),Q(""),ro()}catch(t){Q("Refresh failed: "+((t==null?void 0:t.message)||t))}}async function ro(){try{const t=await Se("get_stats",{days:7}),n=Array.isArray(t.projects)?t.projects[0]:null,r=n==null?void 0:n.posts;if(!r){Zn.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]];Zn.innerHTML=o.map(([e,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${e}</div></div>`).join("")}catch(t){Zn.innerHTML=`<div class="muted">Stats unavailable: ${(t==null?void 0:t.message)||t}</div>`}}function vt(t,n,r){const o=t.textContent;t.disabled=!0,t.textContent=n,r().finally(()=>{t.textContent=o,nl()})}Hn.addEventListener("click",()=>vt(Hn,"Starting…",async()=>{Q("Asking Claude to run setup…");try{const t=await ke.sendMessage({role:"user",content:[{type:"text",text:"Run the social autoposter setup wizard: configure my project (website, what I do, who to target, brand voice) and connect my X/Twitter account. Walk me through it step by step."}]});t!=null&&t.isError?Q("The host rejected the setup request — type “set up social autoposter” in the chat instead."):Q("Setup started in the chat — follow the prompts there, then hit Refresh.")}catch(t){Q("Couldn’t start setup: "+((t==null?void 0:t.message)||t))}}));Nt.addEventListener("click",()=>vt(Nt,"Drafting…",async()=>{Q("Running draft cycle — review prompt will appear in the chat…");try{const t=await Se("draft_cycle");t.review_aborted?Q("Draft review didn't complete — nothing posted."):Q(`Done: drafted ${t.drafted??0}, posted ${t.approved??0}, skipped ${t.skipped??0}.`),ro()}catch(t){Q("Draft cycle failed: "+((t==null?void 0:t.message)||t))}}));Ot.addEventListener("click",()=>vt(Ot,"Working…",async()=>{var n;const t=V!=null&&V.autopilot_on?"disable":"enable";try{const r=await Se("autopilot",{action:t}),o=t==="enable"?!!((n=r.autopilot)!=null&&n.loaded):!r.autopilot_unloaded;Ke({autopilot_on:o}),Q(`Autopilot ${o?"enabled":"disabled"}.`)}catch(r){Q("Autopilot toggle failed: "+((r==null?void 0:r.message)||r))}}));Me.addEventListener("click",()=>vt(Me,"Working…",async()=>{try{if(Le){const t=await Se("setup",{action:"connect_x",confirm:!0});Le=!1,Ke({x_connected:!!t.connected,x_state:t.state||""}),Q(t.summary||(t.connected?"X connected.":"X not connected — see chat."))}else{const t=await Se("setup",{action:"connect_x"});if(t.already_connected){Ke({x_connected:!0}),Q("X already connected.");return}Le=!0,Me.textContent="Confirm: import X session",Q(t.what_will_happen||"This imports your x.com cookies into the autoposter's browser. Click again to confirm.")}}catch(t){Le=!1,Q("Connect X failed: "+((t==null?void 0:t.message)||t))}}));jo.addEventListener("click",()=>vt(jo,"Refreshing…",ah));ke.connect().then(()=>{const t=ke.getHostContext();t&&rl(t),ro()});</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:O().describe("Top safe area inset in pixels."),right:O().describe("Right safe area inset in pixels."),bottom:O().describe("Bottom safe area inset in pixels."),left:O().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),Jf=m({method:u("ui/notifications/host-context-changed"),params:el.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:z(gt).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:R(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:En.describe("App identification (name and version)."),appCapabilities:Mf.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var Vf=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:En.describe("Host application identification and version."),hostCapabilities:Lf.describe("Features and capabilities provided by the host."),hostContext:el.describe("Rich context about the host environment.")}).passthrough(),Wf={target:"draft-2020-12"};async function zo(t,n){let r=t["~standard"];if(r.jsonSchema)return r.jsonSchema[n](Wf);if(r.vendor==="zod"){let{z:o}=await ul(()=>Promise.resolve().then(()=>Em),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 xo(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 Bf(t){let n=document.documentElement;n.setAttribute("data-theme",t),n.style.colorScheme=t}function Kf(t,n=document.documentElement){for(let[r,o]of Object.entries(t))o!==void 0&&n.style.setProperty(r,o)}function Gf(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 Tt=class Tt extends wf{constructor(r,o={},e={autoResize:!0}){super(e);A(this,"_appInfo");A(this,"_capabilities");A(this,"options");A(this,"_hostCapabilities");A(this,"_hostInfo");A(this,"_hostContext");A(this,"_registeredTools",{});A(this,"_initializedSent",!1);A(this,"eventSchemas",{toolinput:Ef,toolinputpartial:Df,toolresult:Ff,toolcancelled:Rf,hostcontextchanged:Jf});A(this,"_everHadListener",new Set);A(this,"_toolHandlersInitialized",!1);A(this,"_onteardown");A(this,"_oncalltool");A(this,"_onlisttools");A(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=e,e.allowUnsafeEval||X({jitless:!0}),this.setRequestHandler(Dn,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(!Tt.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=Sf(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 xo(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 xo(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 zo(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await zo(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(Af,(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(Gu,(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(Ku,(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},Rn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},Vu,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},Ju,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let e=r.tools?Yu:Xu;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},Uf,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},Ai,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},Tf,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},Pf,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},qf,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 xf(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:If}},Vf,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}}};A(Tt,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let qn=Tt;function tl(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 ie=t=>document.getElementById(t),Qf=ie("ver"),Xf=ie("st-proj"),Yf=ie("st-proj-sub"),eh=ie("st-x"),th=ie("st-x-sub"),nh=ie("st-ap"),rh=ie("st-ap-sub"),Hn=ie("btn-setup"),Nt=ie("btn-draft"),Ot=ie("btn-autopilot"),Me=ie("btn-connectx"),jo=ie("btn-refresh"),Zn=ie("stats-grid"),ih=ie("log");let V=null,Le=!1;function Q(t){ih.textContent=t}function nl(){if(!V)return;Qf.innerHTML=V.update_available&&V.latest_version?`v${V.version} · <span class="update">update to ${V.latest_version}</span>`:`v${V.version}`,Xf.textContent=`${V.projects_ready}/${V.projects_total}`,Yf.textContent=V.projects_total===0?"none configured":V.projects.map(r=>r.name+(r.ready?"":" (incomplete)")).join(", "),eh.textContent=V.x_connected?"Connected":"Not connected",th.textContent=V.x_state||"",Me.hidden=V.x_connected,Le||(Me.textContent="Connect X"),nh.textContent=V.autopilot_on?"On":"Off",rh.textContent=V.auto_update_on?"auto-update on":"",Ot.textContent=V.autopilot_on?"Disable autopilot":"Enable autopilot";const t=V.projects_ready>0;Nt.disabled=!t,Ot.disabled=!t;const n=!t;Hn.classList.toggle("primary",n),Nt.classList.toggle("primary",!n)}function Ke(t){V={...V||{},...t},nl()}function oh(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||"",version:t.mcp_version||(V==null?void 0:V.version)||"",latest_version:t.latest_version??null,update_available:!!t.update_available}}const ke=new qn({name:"Social Autoposter Panel",version:"1.0.0"});function rl(t){var n,r,o;t.theme&&Bf(t.theme),(n=t.styles)!=null&&n.variables&&Kf(t.styles.variables),(o=(r=t.styles)==null?void 0:r.css)!=null&&o.fonts&&Gf(t.styles.css.fonts)}ke.onhostcontextchanged=rl;ke.onerror=t=>console.error(t);ke.ontoolresult=t=>{const n=tl(t);n&&typeof n.projects_total=="number"&&Ke(n)};async function Se(t,n={}){const r=await ke.callServerTool({name:t,arguments:n});return tl(r)}async function ah(){Q("Refreshing…");try{const[t,n]=await Promise.all([Se("setup",{status:!0}),Se("autopilot",{action:"status"})]);Ke({...oh(t),autopilot_on:!!n.loaded,auto_update_on:!!n.auto_update_loaded}),Q(""),ro()}catch(t){Q("Refresh failed: "+((t==null?void 0:t.message)||t))}}async function ro(){try{const t=await Se("get_stats",{days:7}),n=Array.isArray(t.projects)?t.projects[0]:null,r=n==null?void 0:n.posts;if(!r){Zn.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]];Zn.innerHTML=o.map(([e,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${e}</div></div>`).join("")}catch(t){Zn.innerHTML=`<div class="muted">Stats unavailable: ${(t==null?void 0:t.message)||t}</div>`}}function vt(t,n,r){const o=t.textContent;t.disabled=!0,t.textContent=n,r().finally(()=>{t.textContent=o,nl()})}Hn.addEventListener("click",()=>vt(Hn,"Starting…",async()=>{Q("Asking Claude to run setup…");try{const t=await ke.sendMessage({role:"user",content:[{type:"text",text:"Run the social autoposter setup wizard: configure my project (website, what I do, who to target, brand voice) and connect my X/Twitter account. Walk me through it step by step."}]});t!=null&&t.isError?Q("The host rejected the setup request — type “set up social autoposter” in the chat instead."):Q("Setup started in the chat — follow the prompts there, then hit Refresh.")}catch(t){Q("Couldn’t start setup: "+((t==null?void 0:t.message)||t))}}));Nt.addEventListener("click",()=>vt(Nt,"Drafting…",async()=>{Q("Drafting… the draft list appears in the chat for review.");try{const n=(await Se("draft_cycle")).drafted??0;Q(n?`Drafted ${n} — review them in the chat and choose which to post.`:"No drafts produced."),ro()}catch(t){Q("Draft cycle failed: "+((t==null?void 0:t.message)||t))}}));Ot.addEventListener("click",()=>vt(Ot,"Working…",async()=>{var n;const t=V!=null&&V.autopilot_on?"disable":"enable";try{const r=await Se("autopilot",{action:t}),o=t==="enable"?!!((n=r.autopilot)!=null&&n.loaded):!r.autopilot_unloaded;Ke({autopilot_on:o}),Q(`Autopilot ${o?"enabled":"disabled"}.`)}catch(r){Q("Autopilot toggle failed: "+((r==null?void 0:r.message)||r))}}));Me.addEventListener("click",()=>vt(Me,"Working…",async()=>{try{if(Le){const t=await Se("setup",{action:"connect_x",confirm:!0});Le=!1,Ke({x_connected:!!t.connected,x_state:t.state||""}),Q(t.summary||(t.connected?"X connected.":"X not connected — see chat."))}else{const t=await Se("setup",{action:"connect_x"});if(t.already_connected){Ke({x_connected:!0}),Q("X already connected.");return}Le=!0,Me.textContent="Confirm: import X session",Q(t.what_will_happen||"This imports your x.com cookies into the autoposter's browser. Click again to confirm.")}}catch(t){Le=!1,Q("Connect X failed: "+((t==null?void 0:t.message)||t))}}));jo.addEventListener("click",()=>vt(jo,"Refreshing…",ah));ke.connect().then(()=>{const t=ke.getHostContext();t&&rl(t),ro()});</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)}@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)}}*{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:16px;display:flex;flex-direction:column;gap:14px}.head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}.title{font-size:16px;font-weight:650;letter-spacing:-.01em}.ver{font-size:12px;color:var(--muted)}.ver .update{color:var(--fg);font-weight:600}.status{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:10px 12px;min-height:64px}.card .k{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)}.card .v{font-size:18px;font-weight:650;margin-top:2px}.card .sub{font-size:11px;color:var(--muted);margin-top:2px}.actions{display:flex;flex-wrap:wrap;gap:8px}button{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:1px solid var(--border);background:var(--card);color:var(--fg);border-radius:8px;padding:8px 14px;font-size:13px;font-weight:550;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:transparent}button[hidden]{display:none}.stats{display:flex;flex-direction:column;gap:8px}.stats-head{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(88px,1fr));gap:8px}.stat{background:var(--card);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}.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/repo.js CHANGED
@@ -23,6 +23,12 @@ export const PYTHON = process.env.SAPS_PYTHON || "python3";
23
23
  export const TMP_DIR = process.env.SAPS_TMP_DIR || "/tmp";
24
24
  // Spawn a process inside the repo, inheriting the repo env (API base + keys
25
25
  // come from the install's environment / .env loaded by the scripts themselves).
26
+ //
27
+ // `onLine` (optional) fires once per COMPLETE line as the child emits output,
28
+ // so a long-running script (e.g. run-twitter-cycle.sh, which can churn for
29
+ // minutes) can be followed live instead of going dark until it exits. The full
30
+ // buffered stdout/stderr are still returned unchanged, so existing callers are
31
+ // unaffected. A throwing sink never breaks the run.
26
32
  export function run(cmd, args, opts = {}) {
27
33
  return new Promise((resolve) => {
28
34
  const child = spawn(cmd, args, {
@@ -31,17 +37,63 @@ export function run(cmd, args, opts = {}) {
31
37
  });
32
38
  let stdout = "";
33
39
  let stderr = "";
40
+ // Per-stream partial-line buffers so onLine fires on whole lines only,
41
+ // regardless of how the OS chunks the pipe reads.
42
+ let outBuf = "";
43
+ let errBuf = "";
44
+ const pump = (chunk, which, buf) => {
45
+ if (!opts.onLine)
46
+ return buf;
47
+ buf += chunk;
48
+ let nl;
49
+ while ((nl = buf.indexOf("\n")) !== -1) {
50
+ const line = buf.slice(0, nl);
51
+ buf = buf.slice(nl + 1);
52
+ try {
53
+ opts.onLine(line, which);
54
+ }
55
+ catch {
56
+ /* a progress sink must never break the wrapped command */
57
+ }
58
+ }
59
+ return buf;
60
+ };
34
61
  let timer;
35
62
  if (opts.timeoutMs) {
36
63
  timer = setTimeout(() => {
37
64
  child.kill("SIGTERM");
38
65
  }, opts.timeoutMs);
39
66
  }
40
- child.stdout.on("data", (d) => (stdout += d.toString()));
41
- child.stderr.on("data", (d) => (stderr += d.toString()));
67
+ child.stdout.on("data", (d) => {
68
+ const s = d.toString();
69
+ stdout += s;
70
+ outBuf = pump(s, "stdout", outBuf);
71
+ });
72
+ child.stderr.on("data", (d) => {
73
+ const s = d.toString();
74
+ stderr += s;
75
+ errBuf = pump(s, "stderr", errBuf);
76
+ });
42
77
  child.on("close", (code) => {
43
78
  if (timer)
44
79
  clearTimeout(timer);
80
+ // Flush any trailing partial line (output with no terminating newline).
81
+ if (opts.onLine) {
82
+ if (outBuf)
83
+ try {
84
+ opts.onLine(outBuf, "stdout");
85
+ }
86
+ catch {
87
+ /* ignore */
88
+ }
89
+ if (errBuf)
90
+ try {
91
+ opts.onLine(errBuf, "stderr");
92
+ }
93
+ catch {
94
+ /* ignore */
95
+ }
96
+ }
45
97
  resolve({ code: code ?? -1, stdout, stderr });
46
98
  });
47
99
  child.on("error", (err) => {
@@ -1 +1 @@
1
- {"version":"1.6.40"}
1
+ {"version":"1.6.44"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "social-autoposter",
3
- "version": "1.6.43",
3
+ "version": "1.6.44",
4
4
  "description": "Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Install as a Claude Code agent skill.",
5
5
  "bin": {
6
6
  "social-autoposter": "bin/cli.js"