social-autoposter 1.6.42 → 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 +201 -98
- package/mcp/dist/panel.html +1 -1
- package/mcp/dist/repo.js +54 -2
- package/mcp/dist/version.json +1 -1
- package/package.json +1 -1
- package/scripts/twitter_browser.py +35 -0
- package/scripts/twitter_post_plan.py +24 -0
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
|
-
//
|
|
5
|
-
// draft_cycle - scan + draft,
|
|
6
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
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
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
269
|
+
return (`[${n}] ${author} (style: ${style})${link}\n` +
|
|
227
270
|
` ${reply.replace(/\n/g, "\n ")}\n` +
|
|
228
271
|
` thread: ${c.candidate_url ?? "?"}`);
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
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
|
|
539
|
-
"
|
|
540
|
-
"
|
|
541
|
-
"
|
|
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
|
-
|
|
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
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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:
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
|
674
|
+
const result = await postApproved(batch_id, plan);
|
|
573
675
|
return jsonContent({
|
|
574
|
-
batch_id
|
|
575
|
-
drafted:
|
|
576
|
-
|
|
577
|
-
skipped:
|
|
578
|
-
edited:
|
|
579
|
-
|
|
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 -----------------------------------
|
package/mcp/dist/panel.html
CHANGED
|
@@ -70,7 +70,7 @@ Boolean requesting whether a visible border and background is provided by the ho
|
|
|
70
70
|
- omitted: host decides border`)});m({method:u("ui/request-display-mode"),params:m({mode: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) =>
|
|
41
|
-
|
|
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) => {
|
package/mcp/dist/version.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.6.
|
|
1
|
+
{"version":"1.6.44"}
|
package/package.json
CHANGED
|
@@ -529,6 +529,35 @@ def _wait_for_reply_textbox(page, total_timeout_ms=45000):
|
|
|
529
529
|
return None
|
|
530
530
|
|
|
531
531
|
|
|
532
|
+
# Post-action interstitials X shows AFTER a successful reply (e.g. the
|
|
533
|
+
# "Unlock more on X" graduated-access sheet). They don't block the post that
|
|
534
|
+
# triggered them, but the sheet stays up and overlays the composer on the NEXT
|
|
535
|
+
# reply in a batch -> spurious reply_box_not_found for posts 2..N. We dismiss
|
|
536
|
+
# them deterministically before looking for the reply box. Targeted by the
|
|
537
|
+
# sheet's CTA label so we never touch a real compose/confirm dialog (those have
|
|
538
|
+
# no "Got it"); best-effort, fast, never raises.
|
|
539
|
+
_OVERLAY_DISMISS_LABELS = ("Got it", "Dismiss")
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _dismiss_known_overlays(page) -> bool:
|
|
543
|
+
"""Click-dismiss any known X nudge sheet currently covering the page.
|
|
544
|
+
|
|
545
|
+
Returns True if something was dismissed. Safe to call on every reply: it is
|
|
546
|
+
a no-op when no known overlay is present and swallows all errors."""
|
|
547
|
+
for label in _OVERLAY_DISMISS_LABELS:
|
|
548
|
+
try:
|
|
549
|
+
btn = page.get_by_role("button", name=label, exact=True).first
|
|
550
|
+
if btn.count() > 0 and btn.is_visible():
|
|
551
|
+
btn.click(timeout=2000)
|
|
552
|
+
page.wait_for_timeout(800)
|
|
553
|
+
print(f"[overlay] dismissed known interstitial via '{label}' button",
|
|
554
|
+
file=sys.stderr)
|
|
555
|
+
return True
|
|
556
|
+
except Exception:
|
|
557
|
+
pass
|
|
558
|
+
return False
|
|
559
|
+
|
|
560
|
+
|
|
532
561
|
def _dump_reply_failure_diag(page, tweet_url):
|
|
533
562
|
"""Dump screenshot + DOM state on reply_box_not_found. Returns a diag dict."""
|
|
534
563
|
import time as _t
|
|
@@ -812,6 +841,12 @@ def reply_to_tweet(tweet_url, text, apply_campaigns=True):
|
|
|
812
841
|
tweet_not_found = True
|
|
813
842
|
break
|
|
814
843
|
|
|
844
|
+
# A nudge sheet left over from the previous reply in this batch
|
|
845
|
+
# (e.g. "Unlock more on X") can sit on top of the composer and
|
|
846
|
+
# mask tweetTextarea_0. Clear it first so the wait below sees the
|
|
847
|
+
# real reply box instead of failing reply_box_not_found.
|
|
848
|
+
_dismiss_known_overlays(page)
|
|
849
|
+
|
|
815
850
|
reply_box = _wait_for_reply_textbox(page, total_timeout_ms=45000)
|
|
816
851
|
if reply_box:
|
|
817
852
|
break
|
|
@@ -858,6 +858,11 @@ def main() -> int:
|
|
|
858
858
|
ap = argparse.ArgumentParser()
|
|
859
859
|
ap.add_argument("--plan", required=True,
|
|
860
860
|
help="Path to the plan JSON file (read-only here)")
|
|
861
|
+
ap.add_argument("--post-unapproved", action="store_true",
|
|
862
|
+
help="Post candidates even when the plan marks them "
|
|
863
|
+
"approved=false. The MCP review path already filters to "
|
|
864
|
+
"approved-only, and autopilot/legacy plans omit the key; "
|
|
865
|
+
"this is the explicit override for an intentional direct run.")
|
|
861
866
|
args = ap.parse_args()
|
|
862
867
|
|
|
863
868
|
plan_path = Path(args.plan)
|
|
@@ -909,6 +914,25 @@ def main() -> int:
|
|
|
909
914
|
fail_reasons: dict[str, int] = {}
|
|
910
915
|
skip_reasons: dict[str, int] = {}
|
|
911
916
|
|
|
917
|
+
# Approval gate. A plan that went through the MCP review carries an
|
|
918
|
+
# `approved` flag per candidate (set in mcp/dist/index.js). Honor it here so
|
|
919
|
+
# a DIRECT `--plan` run — bypassing the elicitation form — can't publish
|
|
920
|
+
# drafts the user never ticked. Plans that never had review (autopilot,
|
|
921
|
+
# legacy) omit the key entirely and pass through untouched. Override with
|
|
922
|
+
# --post-unapproved.
|
|
923
|
+
if not args.post_unapproved:
|
|
924
|
+
_kept = []
|
|
925
|
+
for c in candidates:
|
|
926
|
+
if "approved" in c and not c.get("approved"):
|
|
927
|
+
skipped += 1
|
|
928
|
+
skip_reasons["not_approved"] = skip_reasons.get("not_approved", 0) + 1
|
|
929
|
+
else:
|
|
930
|
+
_kept.append(c)
|
|
931
|
+
if skip_reasons.get("not_approved"):
|
|
932
|
+
print(f"[post] {skip_reasons['not_approved']} candidate(s) skipped: not "
|
|
933
|
+
f"approved in plan (pass --post-unapproved to override)", flush=True)
|
|
934
|
+
candidates = _kept
|
|
935
|
+
|
|
912
936
|
for c in candidates:
|
|
913
937
|
try:
|
|
914
938
|
outcome, reason = post_one(c, picker_assignment=picker_assignment)
|