social-autoposter 1.6.43 → 1.6.45

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,37 @@ 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
+ // NB: lines carry a `[HH:MM:SS] ` timestamp prefix, so don't anchor on ^.
184
+ if ((m = /Selected projects?:\s*(.+)$/.exec(l)))
185
+ return `Selected project: ${m[1]}`;
186
+ if (/phase=phase1\b/.test(l) || /Phase 1: drafting queries/.test(l))
187
+ return "Searching X for fresh threads…";
188
+ if ((m = /Phase 1 complete.*?has (\d+) candidates?/.exec(l)))
189
+ return `Found ${m[1]} candidate thread${m[1] === "1" ? "" : "s"} — ranking them…`;
190
+ if (/phase=phase2a\b/.test(l) || /candidates by virality_score selected/.test(l))
191
+ return "Scoring and ranking candidates…";
192
+ if (/Phase 2b-prep: Claude reading threads and drafting replies/.test(l))
193
+ return "Drafting replies (the long step — this can take a few minutes)…";
194
+ if ((m = /Engagement style assigned:.*?style=(\S+)/.exec(l)))
195
+ return `Drafting in style: ${m[1]}…`;
196
+ if (/DRAFT_ONLY_PLAN=/.test(l))
197
+ return "Drafts ready — assembling the review table…";
198
+ if ((m = /DRAFT_ONLY_BLOCKED=([a-z0-9_]+)/.exec(l)))
199
+ return `Cycle stopped (${m[1]}).`;
200
+ return null;
201
+ }
202
+ async function produceDrafts(project, onProgress) {
172
203
  // Run the real pipeline in DRAFT_ONLY mode: scan -> score -> draft -> link-gen,
173
204
  // then STOP before posting. The script prints `DRAFT_ONLY_PLAN=<path>` and
174
205
  // leaves the plan on disk for us to review + post. SAPS_FORCE_PROJECT scopes
@@ -179,10 +210,57 @@ async function produceDrafts(project) {
179
210
  };
180
211
  if (project)
181
212
  env.SAPS_FORCE_PROJECT = project;
213
+ let step = 0;
214
+ let lastMsg = "";
215
+ // ONE predictable, host-independent place to watch a draft_cycle run, so any
216
+ // agent (or human) debugging "the cycle looks stuck" has an obvious path:
217
+ // ~/social-autoposter/skill/logs/draft_cycle-mcp.log
218
+ // It lives right next to the cycle's own twitter-cycle-*.log. We append the
219
+ // full live cycle output here (not just milestones) plus a clear run banner.
220
+ // Best-effort: a logging failure must never break the cycle.
221
+ const mcpLog = path.join(REPO_DIR, "skill", "logs", "draft_cycle-mcp.log");
222
+ const appendLog = (s) => {
223
+ try {
224
+ fs.appendFileSync(mcpLog, s);
225
+ }
226
+ catch {
227
+ /* ignore — never fail the cycle over a log write */
228
+ }
229
+ };
230
+ try {
231
+ fs.mkdirSync(path.dirname(mcpLog), { recursive: true });
232
+ }
233
+ catch {
234
+ /* ignore */
235
+ }
236
+ appendLog(`\n===== draft_cycle start ${new Date().toISOString()} ` +
237
+ `project=${project ?? "(default)"} =====\n`);
182
238
  const res = await run("bash", ["skill/run-twitter-cycle.sh"], {
183
239
  env,
184
240
  timeoutMs: 900_000, // scan+draft can take several minutes
241
+ // Fan every cycle line out to THREE sinks so progress is never a black box:
242
+ // 1. draft_cycle-mcp.log — the stable, documented, host-independent file.
243
+ // 2. this server's stderr — lands in the host's MCP server log
244
+ // (mcp-server-social-autoposter.log on Desktop), which used to show
245
+ // only the JSON-RPC handshake.
246
+ // 3. the live progress sink — milestone messages under the chat spinner.
247
+ onLine: (line) => {
248
+ const t = line.replace(/\s+$/, "");
249
+ if (t.trim()) {
250
+ appendLog(`${t}\n`);
251
+ console.error(`[draft_cycle] ${t}`);
252
+ }
253
+ if (!onProgress)
254
+ return;
255
+ const msg = cycleProgressMessage(t);
256
+ // Skip consecutive duplicates (a phase can log a couple matching lines).
257
+ if (msg && msg !== lastMsg) {
258
+ lastMsg = msg;
259
+ onProgress(msg, ++step);
260
+ }
261
+ },
185
262
  });
263
+ appendLog(`===== draft_cycle end ${new Date().toISOString()} exit=${res.code} =====\n`);
186
264
  // Prefer the explicit marker; fall back to the newest plan file on disk.
187
265
  const marker = /DRAFT_ONLY_PLAN=\/tmp\/twitter_cycle_plan_(.+)\.json/.exec(res.stdout + "\n" + res.stderr);
188
266
  if (marker && marker[1])
@@ -204,87 +282,28 @@ async function produceDrafts(project) {
204
282
  res.stderr.split("\n").slice(-12).join("\n"),
205
283
  };
206
284
  }
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.
285
+ // Render every draft in a batch as a numbered, human-readable table. This IS the
286
+ // review surface now: the model relays this table to the user and asks which
287
+ // numbers to post / edit, then posts the chosen ones via the `post_drafts` tool.
211
288
  //
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) {
289
+ // We used to gather approvals through MCP elicitation (a checkbox form), but the
290
+ // desktop "Code tab" host doesn't advertise the `elicitation` capability (only
291
+ // `io.modelcontextprotocol/ui`), so the form never rendered and cycles silently
292
+ // posted nothing. Approval is conversational instead — numbers in chat.
293
+ function renderDraftsTable(plan) {
215
294
  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) => {
295
+ return candidates
296
+ .map((c, i) => {
221
297
  const n = i + 1;
222
298
  const author = c.thread_author ? `@${c.thread_author}` : "(unknown thread)";
223
299
  const style = c.engagement_style ?? "?";
224
300
  const reply = c.reply_text ?? "(empty)";
225
301
  const link = c.link_url ? ` · link: ${c.link_url}` : "";
226
- rows.push(`[${n}] ${author} (style: ${style})${link}\n` +
302
+ return (`[${n}] ${author} (style: ${style})${link}\n` +
227
303
  ` ${reply.replace(/\n/g, "\n ")}\n` +
228
304
  ` 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 };
305
+ })
306
+ .join("\n\n");
288
307
  }
289
308
  async function postApproved(batchId, plan) {
290
309
  const approved = (plan.candidates || []).filter((c) => c.approved === true);
@@ -532,25 +551,60 @@ server.registerTool("setup", {
532
551
  return textContent(`Setup failed: ${e.message}`);
533
552
  }
534
553
  });
535
- // ---- draft_cycle: the whole manual loop in one tool -----------------------
554
+ // ---- draft_cycle: scan + draft, then hand the batch to the user for review.
555
+ // Posting is a SEPARATE step (post_drafts) so the user picks by number in chat.
556
+ // This host doesn't support elicitation, so there is no in-tool form: the model
557
+ // relays the table and asks which to post / edit, then calls post_drafts.
536
558
  server.registerTool("draft_cycle", {
537
559
  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.",
560
+ description: "Scan X and draft replies on this machine, then return ALL drafts as a numbered table " +
561
+ "for review. This tool POSTS NOTHING. Show the table to the user and ask which numbers " +
562
+ "to post and which to rewrite, then call `post_drafts` with their decision and the " +
563
+ "returned batch_id. Flow: discover -> draft -> review in chat -> post_drafts.",
542
564
  inputSchema: {
543
565
  project: z
544
566
  .string()
545
567
  .optional()
546
568
  .describe("Which configured project to draft for. Optional when only one project is set up; required when several are."),
547
569
  },
548
- }, async ({ project }) => {
570
+ }, async ({ project }, extra) => {
549
571
  const r = resolveProject(project);
550
572
  if (!r.ok)
551
573
  return textContent(r.message);
552
574
  const proj = r.project;
553
- const drafted = await produceDrafts(proj);
575
+ // Live progress so the chat doesn't sit on a frozen spinner for minutes.
576
+ // Two channels, both best-effort (a sink failure must never fail the cycle):
577
+ // 1. notifications/message — a log line; the host records it (and some
578
+ // clients show it in a log view). Works with no client opt-in.
579
+ // 2. notifications/progress — drives the status text under the running
580
+ // tool. Only valid when the client supplied a progressToken on the
581
+ // request, so it's guarded on that.
582
+ const progressToken = extra?._meta?.progressToken;
583
+ const sendProgress = async (message, step) => {
584
+ try {
585
+ await extra.sendNotification({
586
+ method: "notifications/message",
587
+ params: { level: "info", logger: "draft_cycle", data: message },
588
+ });
589
+ }
590
+ catch {
591
+ /* ignore */
592
+ }
593
+ if (progressToken !== undefined) {
594
+ try {
595
+ await extra.sendNotification({
596
+ method: "notifications/progress",
597
+ params: { progressToken, progress: step, message },
598
+ });
599
+ }
600
+ catch {
601
+ /* ignore */
602
+ }
603
+ }
604
+ };
605
+ const drafted = await produceDrafts(proj, (message, step) => {
606
+ void sendProgress(message, step);
607
+ });
554
608
  if (drafted.blocked || !drafted.batchId) {
555
609
  return textContent(drafted.blocked ?? "No drafts produced.");
556
610
  }
@@ -558,25 +612,107 @@ server.registerTool("draft_cycle", {
558
612
  if (!plan || !(plan.candidates && plan.candidates.length)) {
559
613
  return textContent(`No drafts in batch ${drafted.batchId}.`);
560
614
  }
561
- const review = await reviewDrafts(plan);
562
- writePlan(drafted.batchId, plan);
563
- if (review.aborted && review.approved === 0) {
564
- return jsonContent({
615
+ const count = plan.candidates.length;
616
+ const table = renderDraftsTable(plan);
617
+ const message = `Drafted ${count} ${count === 1 ? "reply" : "replies"} for "${proj}" ` +
618
+ `(batch ${drafted.batchId}). NOTHING has been posted yet.\n\n` +
619
+ `${table}\n\n` +
620
+ `Show this list to the user and ask which to post and which to edit. They can reply ` +
621
+ `however is natural, e.g. "post 1, 3 and 5", "edit 2: <new wording>", "post all", or ` +
622
+ `"skip all". Editing a draft also posts it. Then call the post_drafts tool with ` +
623
+ `batch_id "${drafted.batchId}" and their decision (post: [numbers], edits: [{n, text}], ` +
624
+ `or post_all: true). Do not post anything the user didn't ask for.`;
625
+ return {
626
+ content: [{ type: "text", text: message }],
627
+ structuredContent: {
565
628
  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.",
629
+ drafted: count,
630
+ status: "awaiting_decision",
631
+ },
632
+ };
633
+ });
634
+ // ---- post_drafts: post the user's chosen drafts from a batch ---------------
635
+ // Second half of the manual loop. The user reviewed the table from draft_cycle
636
+ // and said which numbers to post / edit; this posts exactly those. Editing a
637
+ // draft implies posting it. Indices are 1-based, matching the table.
638
+ server.registerTool("post_drafts", {
639
+ title: "Post chosen drafts",
640
+ description: "Post the drafts the user approved from a draft_cycle batch. Pass the batch_id from " +
641
+ "draft_cycle and the user's decision by NUMBER (1-based, matching the table): `post` is " +
642
+ "the list of draft numbers to post as drafted; `edits` rewrites a draft's text before " +
643
+ "posting it (editing implies posting); `post_all` posts every draft. Only the chosen " +
644
+ "drafts post; anything not listed is left unposted. Call this ONLY after the user has " +
645
+ "told you which drafts they want.",
646
+ inputSchema: {
647
+ batch_id: z.string().describe("The batch_id returned by draft_cycle."),
648
+ post: z
649
+ .array(z.number().int().positive())
650
+ .optional()
651
+ .describe("1-based draft numbers to post as drafted, e.g. [1, 3, 5]."),
652
+ edits: z
653
+ .array(z.object({ n: z.number().int().positive(), text: z.string() }))
654
+ .optional()
655
+ .describe("Rewrites: each {n, text} replaces draft n's wording, then posts it."),
656
+ post_all: z.boolean().optional().describe("Post every draft in the batch."),
657
+ },
658
+ }, async ({ batch_id, post, edits, post_all }) => {
659
+ const plan = readPlan(batch_id);
660
+ if (!plan || !(plan.candidates && plan.candidates.length)) {
661
+ return textContent(`No drafts found for batch ${batch_id}. Run draft_cycle again to produce a fresh batch.`);
662
+ }
663
+ const candidates = plan.candidates;
664
+ const total = candidates.length;
665
+ const warnings = [];
666
+ const inRange = (n) => n >= 1 && n <= total;
667
+ // Apply edits first; an edited draft is always posted.
668
+ const approve = new Set();
669
+ let editedCount = 0;
670
+ (edits || []).forEach((e) => {
671
+ if (!inRange(e.n)) {
672
+ warnings.push(`ignored edit for #${e.n}: out of range (1-${total})`);
673
+ return;
674
+ }
675
+ const text = (e.text ?? "").trim();
676
+ if (!text) {
677
+ warnings.push(`ignored empty edit for #${e.n}`);
678
+ return;
679
+ }
680
+ candidates[e.n - 1].reply_text = text;
681
+ approve.add(e.n);
682
+ editedCount++;
683
+ });
684
+ if (post_all) {
685
+ for (let i = 1; i <= total; i++)
686
+ approve.add(i);
687
+ }
688
+ (post || []).forEach((n) => {
689
+ if (inRange(n))
690
+ approve.add(n);
691
+ else
692
+ warnings.push(`ignored #${n}: out of range (1-${total})`);
693
+ });
694
+ candidates.forEach((c, i) => (c.approved = approve.has(i + 1)));
695
+ writePlan(batch_id, plan);
696
+ if (approve.size === 0) {
697
+ return jsonContent({
698
+ batch_id,
699
+ drafted: total,
700
+ posted: 0,
701
+ skipped: total,
702
+ edited: editedCount,
703
+ note: "No drafts selected to post. Nothing was posted.",
704
+ warnings,
570
705
  });
571
706
  }
572
- const posted = await postApproved(drafted.batchId, plan);
707
+ const result = await postApproved(batch_id, plan);
573
708
  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,
709
+ batch_id,
710
+ drafted: total,
711
+ posted: approve.size,
712
+ skipped: total - approve.size,
713
+ edited: editedCount,
714
+ result,
715
+ warnings,
580
716
  });
581
717
  });
582
718
  // ---- 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.45",
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"