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 +234 -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/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,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
|
-
|
|
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
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
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
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
302
|
+
return (`[${n}] ${author} (style: ${style})${link}\n` +
|
|
227
303
|
` ${reply.replace(/\n/g, "\n ")}\n` +
|
|
228
304
|
` 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 };
|
|
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
|
|
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
|
|
539
|
-
"
|
|
540
|
-
"
|
|
541
|
-
"
|
|
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
|
-
|
|
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
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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:
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
|
707
|
+
const result = await postApproved(batch_id, plan);
|
|
573
708
|
return jsonContent({
|
|
574
|
-
batch_id
|
|
575
|
-
drafted:
|
|
576
|
-
|
|
577
|
-
skipped:
|
|
578
|
-
edited:
|
|
579
|
-
|
|
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 -----------------------------------
|
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