social-autoposter 1.6.98 → 1.6.99
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 +84 -13
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_menubar.py +13 -3
- package/mcp/menubar/s4l_state.py +9 -6
- package/mcp/package.json +1 -1
- package/package.json +1 -1
package/mcp/dist/index.js
CHANGED
|
@@ -524,7 +524,7 @@ function renderDraftsTable(plan) {
|
|
|
524
524
|
// Number by FULL-array index (matches post_drafts + the menu bar), then drop
|
|
525
525
|
// already-finished entries so the cards only show what's still pending.
|
|
526
526
|
.map((c, i) => ({ c, n: i + 1 }))
|
|
527
|
-
.filter((e) => e.c.posted !== true && e.c.terminal !== true)
|
|
527
|
+
.filter((e) => e.c.posted !== true && e.c.terminal !== true && e.c.approved !== true)
|
|
528
528
|
// The queue is append-only; newest drafts have the highest stable index.
|
|
529
529
|
// Show those first so review starts with likely-live tweets instead of stale
|
|
530
530
|
// low-number drafts that have been sitting around for hours.
|
|
@@ -637,7 +637,13 @@ async function ensurePostingHandle() {
|
|
|
637
637
|
}
|
|
638
638
|
}
|
|
639
639
|
async function postApproved(batchId, plan) {
|
|
640
|
-
|
|
640
|
+
// Post every card the user APPROVED that hasn't already landed or been ruled out.
|
|
641
|
+
// `approved` is now a DURABLE decision (sticky, never cleared by a later call), so
|
|
642
|
+
// filtering out posted/terminal here makes this idempotent: re-running it only
|
|
643
|
+
// drains the not-yet-posted approved backlog (e.g. a card a restart interrupted),
|
|
644
|
+
// never re-posts a done one. This is what lets the startup backlog-drain and the
|
|
645
|
+
// per-card menu-bar calls share one code path safely.
|
|
646
|
+
const approved = (plan.candidates || []).filter((c) => c.approved === true && c.posted !== true && c.terminal !== true);
|
|
641
647
|
if (approved.length === 0)
|
|
642
648
|
return { attempted: 0, exit_code: 0, summary: "nothing approved" };
|
|
643
649
|
// PREFLIGHT: posting needs a configured @handle, or twitter_browser.py refuses
|
|
@@ -1402,8 +1408,13 @@ tool("post_drafts", {
|
|
|
1402
1408
|
.optional()
|
|
1403
1409
|
.describe("Rewrites: each {n, text} replaces draft n's wording, then posts it."),
|
|
1404
1410
|
post_all: z.boolean().optional().describe("Post every draft in the batch."),
|
|
1411
|
+
reject: z
|
|
1412
|
+
.array(z.number().int().positive())
|
|
1413
|
+
.optional()
|
|
1414
|
+
.describe("1-based draft numbers the user REJECTED. They are marked done and never " +
|
|
1415
|
+
"shown for review again, and are not posted."),
|
|
1405
1416
|
},
|
|
1406
|
-
}, async ({ batch_id, post, edits, post_all }) => {
|
|
1417
|
+
}, async ({ batch_id, post, edits, post_all, reject }) => {
|
|
1407
1418
|
const plan = readPlan(batch_id);
|
|
1408
1419
|
if (!plan || !(plan.candidates && plan.candidates.length)) {
|
|
1409
1420
|
return textContent(`No drafts found for batch ${batch_id}. Run scan_candidates then submit_drafts again to produce a fresh batch.`);
|
|
@@ -1412,6 +1423,25 @@ tool("post_drafts", {
|
|
|
1412
1423
|
const total = candidates.length;
|
|
1413
1424
|
const warnings = [];
|
|
1414
1425
|
const inRange = (n) => n >= 1 && n <= total;
|
|
1426
|
+
// ---- Rejections: durable + final --------------------------------------
|
|
1427
|
+
// A rejected draft is marked terminal so it NEVER re-appears for review and is
|
|
1428
|
+
// never posted. A reject overrides any earlier approve on the same card.
|
|
1429
|
+
const rejected = [];
|
|
1430
|
+
(reject || []).forEach((n) => {
|
|
1431
|
+
if (!inRange(n)) {
|
|
1432
|
+
warnings.push(`ignored reject #${n}: out of range (1-${total})`);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
const c = candidates[n - 1];
|
|
1436
|
+
if (c.posted === true) {
|
|
1437
|
+
warnings.push(`#${n} already posted; not rejecting`);
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
c.terminal = true;
|
|
1441
|
+
c.terminal_reason = "rejected";
|
|
1442
|
+
c.approved = false;
|
|
1443
|
+
rejected.push(n);
|
|
1444
|
+
});
|
|
1415
1445
|
// Apply edits first; an edited draft is always posted.
|
|
1416
1446
|
const approve = new Set();
|
|
1417
1447
|
let editedCount = 0;
|
|
@@ -1440,27 +1470,39 @@ tool("post_drafts", {
|
|
|
1440
1470
|
warnings.push(`ignored #${n}: out of range (1-${total})`);
|
|
1441
1471
|
});
|
|
1442
1472
|
// Cross-surface de-dup: chat and the menu-bar pop-ups can both approve, so
|
|
1443
|
-
// never re-post a candidate the other surface already posted.
|
|
1444
|
-
const
|
|
1473
|
+
// never re-post a candidate the other surface already posted OR ruled out.
|
|
1474
|
+
const alreadyDone = [];
|
|
1445
1475
|
for (const n of Array.from(approve)) {
|
|
1446
|
-
if (candidates[n - 1]?.posted === true) {
|
|
1476
|
+
if (candidates[n - 1]?.posted === true || candidates[n - 1]?.terminal === true) {
|
|
1447
1477
|
approve.delete(n);
|
|
1448
|
-
|
|
1478
|
+
alreadyDone.push(n);
|
|
1449
1479
|
}
|
|
1450
1480
|
}
|
|
1451
|
-
if (
|
|
1452
|
-
warnings.push(`already posted (skipped): ${
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1481
|
+
if (alreadyDone.length) {
|
|
1482
|
+
warnings.push(`already posted/decided (skipped): ${alreadyDone.sort((a, b) => a - b).join(", ")}`);
|
|
1483
|
+
}
|
|
1484
|
+
// STICKY approve: record the approval DURABLY and never clear another card's
|
|
1485
|
+
// prior approval. The old `c.approved = approve.has(i+1)` reset every card on
|
|
1486
|
+
// each call, so a later post_drafts for a different card dropped a
|
|
1487
|
+
// restart-interrupted approved card back into "pending". postApproved filters
|
|
1488
|
+
// posted/terminal, so the approved set only ever drains what's genuinely left.
|
|
1489
|
+
approve.forEach((n) => {
|
|
1490
|
+
const c = candidates[n - 1];
|
|
1491
|
+
if (c)
|
|
1492
|
+
c.approved = true;
|
|
1493
|
+
});
|
|
1455
1494
|
writePlan(batch_id, plan);
|
|
1456
1495
|
if (approve.size === 0) {
|
|
1457
1496
|
return jsonContent({
|
|
1458
1497
|
batch_id,
|
|
1459
1498
|
drafted: total,
|
|
1460
1499
|
posted: 0,
|
|
1500
|
+
rejected: rejected.length,
|
|
1461
1501
|
skipped: total,
|
|
1462
1502
|
edited: editedCount,
|
|
1463
|
-
note:
|
|
1503
|
+
note: rejected.length
|
|
1504
|
+
? `Rejected ${rejected.length} draft(s); they won't be shown for review again. Nothing was posted.`
|
|
1505
|
+
: "No drafts selected to post. Nothing was posted.",
|
|
1464
1506
|
warnings,
|
|
1465
1507
|
});
|
|
1466
1508
|
}
|
|
@@ -1478,6 +1520,7 @@ tool("post_drafts", {
|
|
|
1478
1520
|
drafted: total,
|
|
1479
1521
|
posted: actuallyPosted,
|
|
1480
1522
|
approved: approve.size,
|
|
1523
|
+
rejected: rejected.length,
|
|
1481
1524
|
skipped: total - actuallyPosted,
|
|
1482
1525
|
edited: editedCount,
|
|
1483
1526
|
result,
|
|
@@ -3056,7 +3099,10 @@ tool("submit_drafts", {
|
|
|
3056
3099
|
added++;
|
|
3057
3100
|
}
|
|
3058
3101
|
writePlan(REVIEW_QUEUE_ID, { candidates: queue });
|
|
3059
|
-
|
|
3102
|
+
// Pending = NOT YET DECIDED. A card that's posted, terminal (rejected/dead), OR
|
|
3103
|
+
// already approved is a settled decision and must never be re-presented for
|
|
3104
|
+
// review — approved ones just proceed to post (see drainApprovedBacklog).
|
|
3105
|
+
const pending = queue.filter((c) => c.posted !== true && c.terminal !== true && c.approved !== true);
|
|
3060
3106
|
// Drafts queued = the pipeline verified end-to-end without posting. This is the
|
|
3061
3107
|
// onboarding "draft_verified" terminal goal (formerly completed by draft_cycle).
|
|
3062
3108
|
if (added > 0)
|
|
@@ -3266,6 +3312,24 @@ registerAppResource(server, "S4L product link", PRODUCT_LINK_URI, { mimeType: RE
|
|
|
3266
3312
|
},
|
|
3267
3313
|
],
|
|
3268
3314
|
}));
|
|
3315
|
+
// Post any cards the user APPROVED that never landed — e.g. a restart killed the
|
|
3316
|
+
// batch mid-way. "Proceed to post the already-approved items." postApproved is
|
|
3317
|
+
// idempotent (it filters posted/terminal), so this only drains the genuine
|
|
3318
|
+
// backlog and never double-posts. Best-effort; never throws.
|
|
3319
|
+
async function drainApprovedBacklog() {
|
|
3320
|
+
try {
|
|
3321
|
+
const plan = readPlan(REVIEW_QUEUE_ID);
|
|
3322
|
+
const cands = plan?.candidates || [];
|
|
3323
|
+
const backlog = cands.filter((c) => c.approved === true && c.posted !== true && c.terminal !== true);
|
|
3324
|
+
if (!backlog.length)
|
|
3325
|
+
return;
|
|
3326
|
+
console.error(`[post] draining ${backlog.length} approved-but-unposted card(s) left from before`);
|
|
3327
|
+
await postApproved(REVIEW_QUEUE_ID, plan);
|
|
3328
|
+
}
|
|
3329
|
+
catch (e) {
|
|
3330
|
+
console.error("[post] drainApprovedBacklog error:", e?.message || e);
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3269
3333
|
async function main() {
|
|
3270
3334
|
initSentry();
|
|
3271
3335
|
// Tee the verbatim stdout/stderr of every pipeline subprocess to the s4l
|
|
@@ -3330,6 +3394,13 @@ async function main() {
|
|
|
3330
3394
|
void startLocalPanel()
|
|
3331
3395
|
.then((url) => console.error(`[social-autoposter-mcp] panel loopback ready at ${url}`))
|
|
3332
3396
|
.catch((e) => console.error("[social-autoposter-mcp] panel loopback start failed:", e?.message || e));
|
|
3397
|
+
// Resume posting any approved-but-unposted cards a prior run/restart left behind.
|
|
3398
|
+
// Delayed so the runtime + harness Chrome have settled; never blocks boot.
|
|
3399
|
+
{
|
|
3400
|
+
const t = setTimeout(() => void drainApprovedBacklog(), 30_000);
|
|
3401
|
+
if (typeof t.unref === "function")
|
|
3402
|
+
t.unref();
|
|
3403
|
+
}
|
|
3333
3404
|
// Ensure the macOS menu bar mini-dashboard is installed + running. Idempotent
|
|
3334
3405
|
// and cheap when already present, so existing installs pick it up on the next
|
|
3335
3406
|
// Claude restart without re-provisioning. Best-effort: never blocks boot.
|
package/mcp/dist/version.json
CHANGED
package/mcp/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"dxt_version": "0.1",
|
|
3
3
|
"name": "social-autoposter",
|
|
4
4
|
"display_name": "S4L",
|
|
5
|
-
"version": "1.6.
|
|
5
|
+
"version": "1.6.99",
|
|
6
6
|
"description": "Draft, review, approve, and autopilot X/Twitter posts. Thin desktop client over the S4L pipeline.",
|
|
7
7
|
"long_description": "A guided assistant that drafts, reviews, and autopilots X/Twitter posts.\nTo get started:\n1. Click **Configure** and set every tool permission to **Always Allow**.\n2. Copy this prompt: **Set me up on S4L end to end**.\n3. Quit fully with CMD+Q, restart Claude, and paste the prompt into a new chat.",
|
|
8
8
|
"author": {
|
|
@@ -544,10 +544,20 @@ class S4LMenuBar(rumps.App):
|
|
|
544
544
|
|
|
545
545
|
def _on_card_decision(self, batch, decision):
|
|
546
546
|
# Runs on the main thread the INSTANT a card is approved/rejected. An
|
|
547
|
-
# approved card is enqueued for immediate posting; a
|
|
548
|
-
#
|
|
549
|
-
#
|
|
547
|
+
# approved card is enqueued for immediate posting; a REJECTED card is
|
|
548
|
+
# persisted (marked done so it's never re-shown for review) on a quick
|
|
549
|
+
# background thread. We never block inline here — posting can take minutes
|
|
550
|
+
# and would freeze the card UI while the user reviews the rest of the stack.
|
|
550
551
|
if not decision.get("approved"):
|
|
552
|
+
n = decision.get("n")
|
|
553
|
+
|
|
554
|
+
def _persist_reject():
|
|
555
|
+
try:
|
|
556
|
+
st.post_drafts(batch, reject=[n], timeout=30)
|
|
557
|
+
except Exception:
|
|
558
|
+
pass
|
|
559
|
+
|
|
560
|
+
threading.Thread(target=_persist_reject, daemon=True).start()
|
|
551
561
|
return
|
|
552
562
|
with self._review_lock:
|
|
553
563
|
self._posts_outstanding += 1
|
package/mcp/menubar/s4l_state.py
CHANGED
|
@@ -350,10 +350,12 @@ def read_plan(plan_path):
|
|
|
350
350
|
|
|
351
351
|
|
|
352
352
|
def review_drafts(plan):
|
|
353
|
-
"""Flatten a plan into the card model: only
|
|
353
|
+
"""Flatten a plan into the card model: only UNDECIDED candidates. A card that's
|
|
354
|
+
posted, terminal (rejected/dead), or already approved is a settled decision and
|
|
355
|
+
must never be re-presented for review (approved ones proceed to post)."""
|
|
354
356
|
out = []
|
|
355
357
|
for i, c in enumerate(((plan or {}).get("candidates") or [])):
|
|
356
|
-
if c.get("posted") is True or c.get("terminal") is True:
|
|
358
|
+
if c.get("posted") is True or c.get("terminal") is True or c.get("approved") is True:
|
|
357
359
|
continue
|
|
358
360
|
out.append(
|
|
359
361
|
{
|
|
@@ -370,11 +372,12 @@ def review_drafts(plan):
|
|
|
370
372
|
return out
|
|
371
373
|
|
|
372
374
|
|
|
373
|
-
def post_drafts(batch_id, post=None, edits=None, timeout=900, activity_label=None):
|
|
374
|
-
"""Post
|
|
375
|
-
|
|
375
|
+
def post_drafts(batch_id, post=None, edits=None, reject=None, timeout=900, activity_label=None):
|
|
376
|
+
"""Post / reject drafts via the loopback tool. `post` = 1-based numbers to post
|
|
377
|
+
as-is; `edits` = [{n, text}] to rewrite then post; `reject` = numbers to mark
|
|
378
|
+
DONE so they're never shown for review again (not posted). Returns the parsed
|
|
376
379
|
result, or None if the loopback is unreachable (Claude Desktop closed)."""
|
|
377
|
-
args = {"batch_id": batch_id, "post": post or [], "edits": edits or []}
|
|
380
|
+
args = {"batch_id": batch_id, "post": post or [], "edits": edits or [], "reject": reject or []}
|
|
378
381
|
if activity_label:
|
|
379
382
|
args["__saps_activity_label"] = activity_label
|
|
380
383
|
return loopback_tool("post_drafts", args, timeout=timeout)
|
package/mcp/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m13v/social-autoposter-mcp",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.99",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Desktop MCP client for social-autoposter (X/Twitter rail): manual draft/review/approve loop, autopilot control, and stats. Thin wrapper over the existing pipeline scripts.",
|
|
6
6
|
"license": "MIT",
|
package/package.json
CHANGED